MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的属性访问修饰符与可见性规则

2021-04-113.2k 阅读

C#中的属性访问修饰符与可见性规则

一、属性访问修饰符概述

在C#编程中,属性访问修饰符对于控制类成员(包括字段、方法、属性等)的可见性起着至关重要的作用。通过合理使用这些修饰符,开发者能够有效地管理代码的封装性、安全性以及模块之间的交互。属性访问修饰符决定了类成员在不同的代码上下文(如同一类内部、同一程序集内的其他类、不同程序集的类等)中的可访问程度。

C#提供了多种属性访问修饰符,常见的包括 publicprivateprotectedinternal 以及 protected internal。每一种修饰符都有其特定的访问规则,下面将详细介绍这些修饰符及其背后的本质。

二、public 访问修饰符

  1. 可见性规则 public 是访问级别最高的修饰符。被 public 修饰的类成员可以从任何其他代码访问,无论这些代码是否在同一程序集内,也无论它们是在派生类还是非派生类中。这意味着 public 成员在整个应用程序范围内都是可见的。

  2. 代码示例

public class PublicClass
{
    public int PublicField;
    public void PublicMethod()
    {
        Console.WriteLine("This is a public method.");
    }
}

class Program
{
    static void Main()
    {
        PublicClass publicObj = new PublicClass();
        publicObj.PublicField = 10;
        publicObj.PublicMethod();
    }
}

在上述示例中,PublicClass 中的 PublicFieldPublicMethod 都被声明为 public。在 Main 方法中,我们可以创建 PublicClass 的实例,并直接访问其 public 成员。这种广泛的可见性在需要对外提供接口或者共享数据和功能时非常有用。例如,在开发库或者框架时,一些核心的功能和数据可能需要以 public 的形式暴露给外部使用者。

  1. 使用场景 通常用于定义库或者框架的公共接口。例如,在开发一个图形绘制库时,绘制图形的主要方法可能会被定义为 public,以便其他开发者在他们的项目中使用这些绘图功能。

三、private 访问修饰符

  1. 可见性规则 private 修饰符提供了最严格的访问限制。被 private 修饰的类成员只能在定义它们的类内部访问,在类的外部,包括派生类和同一程序集内的其他类,都无法直接访问 private 成员。这是实现类的封装性的重要手段,将类的内部状态和实现细节隐藏起来,只对外提供必要的公共接口。

  2. 代码示例

class PrivateClass
{
    private int privateField;
    private void PrivateMethod()
    {
        Console.WriteLine("This is a private method.");
    }

    public void PublicMethod()
    {
        privateField = 20;
        PrivateMethod();
    }
}

class Program
{
    static void Main()
    {
        PrivateClass privateObj = new PrivateClass();
        // 以下代码将导致编译错误
        // privateObj.privateField = 10;
        // privateObj.PrivateMethod();
        privateObj.PublicMethod();
    }
}

在这个例子中,PrivateClass 中的 privateFieldPrivateMethod 都是 private 的。在 Main 方法中,尝试直接访问这些 private 成员会导致编译错误。然而,在 PublicMethod 内部,由于它属于 PrivateClass 类的一部分,所以可以正常访问 private 成员。

  1. 使用场景 常用于隐藏类的内部实现细节。例如,在一个银行账户类中,账户密码的存储字段可以被定义为 private,以确保只有账户类内部的验证方法可以访问和处理密码相关的逻辑,而外部代码无法直接获取或修改密码。

四、protected 访问修饰符

  1. 可见性规则 protected 修饰符允许类成员在定义它们的类以及该类的任何派生类中访问。在同一程序集内的非派生类中,protected 成员是不可访问的。这为类的继承层次结构提供了一种保护机制,使得基类可以将一些成员标记为受保护,供派生类使用,但又限制了同一程序集内其他无关类的访问。

  2. 代码示例

class BaseClass
{
    protected int protectedField;
    protected void ProtectedMethod()
    {
        Console.WriteLine("This is a protected method.");
    }
}

class DerivedClass : BaseClass
{
    public void AccessProtectedMembers()
    {
        protectedField = 30;
        ProtectedMethod();
    }
}

class Program
{
    static void Main()
    {
        DerivedClass derivedObj = new DerivedClass();
        derivedObj.AccessProtectedMembers();

        BaseClass baseObj = new BaseClass();
        // 以下代码将导致编译错误
        // baseObj.protectedField = 10;
        // baseObj.ProtectedMethod();
    }
}

在上述代码中,BaseClass 定义了 protectedFieldProtectedMethodDerivedClass 继承自 BaseClass,在 DerivedClassAccessProtectedMembers 方法中,可以访问 BaseClassprotected 成员。然而,在 Main 方法中,通过 BaseClass 的实例直接访问 protected 成员会导致编译错误。

  1. 使用场景 当基类有一些成员希望派生类能够使用,但又不想让同一程序集内的其他无关类访问时,就可以使用 protected 修饰符。例如,在一个图形基类中,可能定义了一些用于图形绘制的基础算法和数据,这些对于具体的图形派生类(如圆形、矩形等)是有用的,但对于其他不相关的类则不需要暴露。

五、internal 访问修饰符

  1. 可见性规则 internal 修饰符表示类成员在同一程序集内是可见的,但在不同程序集内不可见。这意味着同一项目中的所有类都可以访问 internal 成员,但如果将该项目编译成库,并在另一个项目中引用,另一个项目中的类无法访问这些 internal 成员。这种访问级别在控制程序集内部的代码交互方面非常有用。

  2. 代码示例 假设我们有两个项目:ProjectAProjectB。在 ProjectA 中有以下代码:

// ProjectA 中的代码
internal class InternalClass
{
    internal int internalField;
    internal void InternalMethod()
    {
        Console.WriteLine("This is an internal method.");
    }
}

class ProgramInA
{
    static void Main()
    {
        InternalClass internalObj = new InternalClass();
        internalObj.internalField = 40;
        internalObj.InternalMethod();
    }
}

ProjectB 中尝试访问 ProjectA 中的 InternalClass 成员:

// ProjectB 中的代码
class ProgramInB
{
    static void Main()
    {
        // 以下代码将导致编译错误,因为 InternalClass 及其成员在不同程序集内不可见
        // InternalClass internalObj = new InternalClass();
        // internalObj.internalField = 10;
        // internalObj.InternalMethod();
    }
}

ProjectA 内部,InternalClassinternal 成员可以被正常访问。但在 ProjectB 中,即使引用了 ProjectA,也无法访问这些 internal 成员。

  1. 使用场景 常用于项目内部的一些辅助类或者工具方法。这些类和方法对于项目内部的其他模块是有用的,但不希望被外部项目直接访问。例如,在一个大型的企业级应用项目中,可能有一些内部的日志记录类,这些类只在项目内部使用,不需要暴露给外部,就可以使用 internal 修饰符。

六、protected internal 访问修饰符

  1. 可见性规则 protected internalprotectedinternal 两种访问修饰符的组合。被 protected internal 修饰的类成员在同一程序集内的任何类中以及不同程序集内的派生类中是可见的。这意味着,它同时具备了 protectedinternal 的访问特性,为类成员的访问控制提供了更灵活的选择。

  2. 代码示例 假设我们有 Assembly1Assembly2 两个程序集。在 Assembly1 中有以下代码:

// Assembly1 中的代码
public class BaseWithProtectedInternal
{
    protected internal int protectedInternalField;
    protected internal void ProtectedInternalMethod()
    {
        Console.WriteLine("This is a protected internal method.");
    }
}

class ClassInAssembly1
{
    public void AccessProtectedInternalMembers()
    {
        BaseWithProtectedInternal obj = new BaseWithProtectedInternal();
        obj.protectedInternalField = 50;
        obj.ProtectedInternalMethod();
    }
}

Assembly2 中有一个派生自 BaseWithProtectedInternal 的类:

// Assembly2 中的代码
public class DerivedInAssembly2 : BaseWithProtectedInternal
{
    public void AccessProtectedInternalMembersInDerived()
    {
        protectedInternalField = 60;
        ProtectedInternalMethod();
    }
}

Assembly1 中,ClassInAssembly1 可以访问 BaseWithProtectedInternalprotected internal 成员,因为它们在同一程序集内。在 Assembly2 中,DerivedInAssembly2 可以访问 BaseWithProtectedInternalprotected internal 成员,因为它是派生类。

  1. 使用场景 当希望某些成员在同一程序集内可以自由访问,同时在不同程序集的派生类中也能访问时,protected internal 就非常合适。例如,在开发一个可扩展的框架时,可能有一些核心类的部分成员既需要在框架内部被广泛使用,又希望外部开发者在派生类中能够访问和扩展这些功能。

七、属性访问修饰符在属性中的应用

  1. 属性的访问修饰符设置 属性在C#中是一种特殊的成员,它结合了字段和方法的特性。属性可以有不同的访问修饰符来控制其获取(get 访问器)和设置(set 访问器)的可见性。例如,我们可以定义一个属性,使其 get 访问器是 public,而 set 访问器是 private,这样外部代码只能读取属性的值,而不能直接设置它。
class PropertyAccessExample
{
    private int privateValue;

    public int PublicReadOnlyProperty
    {
        get { return privateValue; }
        private set { privateValue = value; }
    }
}

class Program
{
    static void Main()
    {
        PropertyAccessExample obj = new PropertyAccessExample();
        // 可以读取属性值
        int value = obj.PublicReadOnlyProperty;
        // 以下代码将导致编译错误,因为 set 访问器是 private
        // obj.PublicReadOnlyProperty = 10;
    }
}

在上述代码中,PublicReadOnlyPropertyget 访问器是 public,允许外部代码读取属性值,而 set 访问器是 private,防止外部代码直接设置属性值。

  1. 不同访问修饰符组合的意义
    • public getpublic set:属性完全可读写,外部代码可以自由获取和设置属性值。这种情况适用于一些公开的数据,例如用户的姓名等信息。
    • public getprivate set:属性只读,外部代码只能获取值,不能直接设置。常用于一些内部状态信息,外部只需要了解其值,而不需要修改,例如对象的创建时间。
    • private getpublic set:属性只写,外部代码只能设置值,不能获取。这种情况相对较少见,但在某些场景下,例如只需要外部提供数据,而内部进行处理,不希望外部获取当前状态时会用到。
    • protected getpublic set:在派生类中可以读取属性值,外部代码只能设置值。例如在基类中定义一个属性,派生类可能需要根据这个属性进行一些计算,但外部代码只负责提供数据。

八、属性访问修饰符与继承中的可见性

  1. 基类成员在派生类中的可见性 当派生类继承自基类时,基类成员的可见性规则会影响派生类对这些成员的访问。public 成员在派生类中仍然是 publicprotected 成员在派生类中仍然是 protectedprivate 成员在派生类中不可见。
class BaseForInheritance
{
    public int publicBaseField;
    protected int protectedBaseField;
    private int privateBaseField;
}

class DerivedFromBase : BaseForInheritance
{
    public void AccessBaseMembers()
    {
        publicBaseField = 100;
        protectedBaseField = 200;
        // 以下代码将导致编译错误,因为 privateBaseField 不可见
        // privateBaseField = 300;
    }
}

在上述示例中,DerivedFromBase 可以访问 BaseForInheritancepublicprotected 成员,但不能访问 private 成员。

  1. 重写基类成员时的访问修饰符规则 当派生类重写基类的虚方法或属性时,重写成员的访问修饰符必须与基类中被重写成员的访问修饰符一致或者更宽松。例如,如果基类中的虚方法是 protected,派生类中重写的方法可以是 protected 或者 public,但不能是 private
class BaseWithVirtual
{
    protected virtual void VirtualMethod()
    {
        Console.WriteLine("Base virtual method.");
    }
}

class DerivedOverride : BaseWithVirtual
{
    // 正确,访问修饰符更宽松
    public override void VirtualMethod()
    {
        Console.WriteLine("Derived overridden method.");
    }
}

在这个例子中,DerivedOverride 重写 BaseWithVirtualVirtualMethod 时,将访问修饰符从 protected 改为 public,这是允许的。

九、属性访问修饰符与反射

  1. 反射对不同访问修饰符成员的访问 反射是C#中一种强大的机制,它允许程序在运行时检查和操作类型的元数据以及对象的成员。通过反射,即使是 private 成员,在特定情况下也可以被访问。然而,这种访问需要特殊的权限,并且绕过了正常的访问控制机制。
class ReflectClass
{
    private int privateReflectField;

    public ReflectClass(int value)
    {
        privateReflectField = value;
    }
}

class Program
{
    static void Main()
    {
        ReflectClass reflectObj = new ReflectClass(70);
        Type type = reflectObj.GetType();
        FieldInfo fieldInfo = type.GetField("privateReflectField", BindingFlags.NonPublic | BindingFlags.Instance);
        if (fieldInfo != null)
        {
            int value = (int)fieldInfo.GetValue(reflectObj);
            Console.WriteLine($"Value of private field: {value}");
        }
    }
}

在上述代码中,通过反射获取并访问了 ReflectClass 中的 private 字段 privateReflectFieldGetField 方法的第二个参数指定了要获取的字段的绑定标志,包括非公共成员和实例成员。

  1. 反射访问的安全性和注意事项 虽然反射可以突破正常的访问修饰符限制,但这也带来了一定的安全风险。在使用反射访问非公共成员时,要确保代码的安全性,避免恶意代码利用反射进行非法操作。此外,反射的性能相对较低,频繁使用反射访问成员可能会影响程序的性能。在实际开发中,应该谨慎使用反射来访问非公共成员,只有在确实需要绕过正常访问控制的特殊情况下才使用。

十、属性访问修饰符的最佳实践

  1. 最小化可见性原则 在设计类和成员时,应该遵循最小化可见性原则。即只将成员的可见性设置为满足需求的最小程度。例如,如果一个成员只在类内部使用,就应该将其声明为 private;如果只在同一程序集内使用,就使用 internal。这样可以提高代码的封装性和安全性,减少外部代码对内部实现细节的依赖,降低代码维护的难度。

  2. 清晰的接口设计 对于对外提供的接口(通常是 public 成员),应该设计得清晰明了,易于理解和使用。接口的设计应该遵循一定的规范和约定,并且提供适当的文档说明。同时,要避免过度暴露内部实现细节,保持接口的稳定性,以防止在后续的版本更新中对外部使用代码造成影响。

  3. 继承层次中的访问控制 在设计继承层次结构时,要合理使用 protectedprotected internal 修饰符。通过这些修饰符,基类可以将一些成员提供给派生类使用,同时又能控制这些成员的访问范围。在重写基类成员时,要注意访问修饰符的规则,确保重写成员的可见性符合设计意图,并且不会破坏继承层次的访问控制逻辑。

  4. 属性的访问控制设计 对于属性,要根据其用途和需求合理设置 getset 访问器的访问修饰符。如果属性表示只读数据,就应该将 set 访问器设置为 private 或者不提供 set 访问器。如果属性表示可读写数据,要确保 set 访问器的逻辑是安全和合理的,避免外部代码对属性值进行非法修改。

总之,熟练掌握C#中的属性访问修饰符与可见性规则,并遵循最佳实践,对于编写高质量、可维护、安全的C#程序至关重要。通过合理运用这些规则,可以有效地管理代码的结构和模块之间的交互,提高代码的可重用性和可扩展性。