受保护的只读字段 vs 受保护的属性

19

我有一个抽象类,我想在其受保护的构造函数中初始化一个只读字段。我希望这个只读字段可以在派生类中使用。

按照我的习惯,我将所有字段都设为私有并公开属性,并按以下方式实现了它:

abstract class Foo
{
    private readonly int _field;

    protected Foo(int field)
    {
        _field = field;
    }

    protected int Field
    {
        get { return _field; }
    }
}

但是我在想,将此字段保持为私有是否真的有很多优势。我知道属性的优点,并且有几个SO问题涉及此问题,但它们侧重于公共字段而不是受保护的字段。

那么,我应该切换到下面的实现吗? 在任何一种情况下,需要注意哪些考虑因素和优缺点?

abstract class Foo
{
    protected readonly int _field;

    protected Foo(int field)
    {
        _field = field;
    }
}

1
在这种情况下,publicprotected之间实际上没有区别。两者都可以在声明程序集外部看到。因此,关于public readonly X Ypublic X Y{get;}的所有争论都适用。 - CodesInChaos
7个回答

13

派生类仍然是原始代码的“用户”;对于他们,字段也应该被封装起来。

您应该将基类视为安全且可扩展的 API,而不仅仅是公开其内部的类。保持字段私有化-除了其他任何事情之外,它允许基类更改如何生成属性值 :)


5

我会留下实现或选择以下方案:

protected Field {get; private set;}

属性(Properties)比字段(Fields)更具有未来性。你可以在父类上更改实现而不影响子类。如果没有属性,你就必须始终将字段设为只读。使用字段还有一个缺点,它们会损害封装性:类描述了字段的确切实现。

因此,是的,请保留封装性。

唯一考虑直接使用字段以提高可读性的情况是高度耦合的类,例如状态模式中的状态,但在这种情况下,这些类本身将是私有的。


1

我能想到两个理由,支持使用受保护的属性而不是受保护的字段。首先,考虑以下示例:

public class BaseClass 
{
    protected readonly List<string> _someValues;
}

public class InheritedClass : BaseClass
{
    public void NeedsThoseValues()
    {
         DoSomethingWith(_someValues);
    }
}

现在您决定更改基类以使用延迟实例化:
public class BaseClass 
{
    protected readonly Lazy<List<string>> _someValues;
}

现在继承类必须更改以调用_someValues.Value。那些继承类真的需要更改吗?如果字段是私有的,并作为属性暴露给继承类,更改基类不会破坏继承类:
public class BaseClass 
{
    private readonly Lazy<List<string>> _someValues;

    protected List<string> SomeValues => _someValues.Value;
}

这需要我们改变我们心中继承类的形象。我们可能开始将其视为在较小的房子周围建造一个更大的房子。所有原来在较小的房子里的东西都在更大的房子里,所以它们实际上是一个大房子。没有理由将内部房屋隐藏起来不让外部房屋看到。
实际上并不是这样的。基类是它自己独立的实体,存在于更大的房子中。为了在多个房子(多个继承类)中重用它,我们需要封装它,使得继承类对它了解得越少越好,就像它们依赖的任何其他类一样。
这有时会创建一种奇怪的关系。我们试图防止继承类和基类的实现细节之间过度耦合,但它们却是耦合的,因为继承类不能没有基类存在。这引发了一个问题 - 为什么继承类需要与基类具有这种关系呢?为什么不将基类所做的事情分离成它自己的类,用抽象(比如接口)表示,并在需要时注入它呢?
这就是偏向组合而非继承的思想。很多时候,继承被用于在类(基类和子类)之间共享功能,但这并不是继承的目的。如果我们有两个不同的功能区域,我们应该使用两个不同的类来实现,并且一个类可以依赖于另一个类。如果我们通过继承来实现这一点,当我们的类需要依赖于另一个类时,我们将遇到麻烦。我们不能再给它一个基类了。除非我们组合不同的类以协同工作,否则我们可能会开始做一些非常恶劣的事情,比如将更多的功能添加到现有的基类中或创建更多的继承级别。这样做会让人难以理解,最终会把我们逼入死角。
另一个原因是,当某人在基类中看到_someValues的引用时,他们会认为它是在使用它的类中声明的字段,因为这是更常见的约定。这不会造成太大的混淆,但他们需要花费更长的时间来弄清楚。
这些偏向于使用受保护的只读属性而不是受保护的只读字段的原因,在许多特定情况下可能并不是问题。但很难事先知道,因此最好选择首选的做法,并养成习惯,同时理解为什么要这样做。

1

C# 6.0 实现了只读属性。 实现方法如下:

protected Foo(int field)
{
    Field = field;
}

protected int Field { get; }

它只能从构造函数中设置和修改,与只读字段相同。它与具有私有设置的属性不同。 - VladL

0
abstract class Foo
{
    protected readonly int _field;

    protected Foo(int field)
    {
        _field = field;
    }
}

如果你希望派生类知道它,那么这将是合适的。不属于类型Foo的类将无法访问_field


派生类仍然可以是外部的。 - Eli Algranti
“External” 指的是不属于 Foo 类型的类。 - Lews Therin

0

使用反射,您可以轻松地覆盖只读字段。使用属性会更困难,因为该字段是隐藏的(但仍然可以做到)。所以我更喜欢使用属性,因为它更加清晰,而且您可以更改getter而不会出现任何问题。

如果您考虑性能:大多数情况下,属性都是内联的。

覆盖只读字段

class Program
{
    static void Main(string[] args)
    {
        Test t = new Test();
        t.OverrideReadonly("TestField", 5);
        t.OverrideReadonly("TestField2", 6);
        t.OverrideReadonly("TestField3", new Test());
    }
}

class Test
{
    protected readonly Int32 TestField = 1;
    protected readonly Int32 TestField2 = 2;
    protected readonly Test TestField3 = null;

    public void OverrideReadonly(String fieldName, Object value)
    {
        FieldInfo field = typeof(Test).GetField(fieldName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        field.SetValue(this, value);
    }
}

我不怀疑你关于使用反射覆盖只读字段的说法,但是能否提供一个参考或示例会更好。我怀疑的部分是它是否可以 "轻松地"完成。 - Zaid Masud
谢谢,但显然一个私有只读字段同样容易更改。 - Zaid Masud
@ZaidMasud 正如我所说,这就是更好地使用具有私有setter的属性的原因。那么更改字段就不再那么容易了,并且当您想要更改getter中的某些内容时,它具有一些优势(例如,如果您用检索远程实例的代码替换后备变量)。 - Felix K.
我的唯一观点是,使用反射,私有只读字段似乎和受保护的只读字段一样容易更改。 - Zaid Masud
@ZaidMasud 这是正确的,但有一点不同。作为继承者,您不知道 MyPrivateField 字段的名称是否会在软件的下一个版本中更改,因此通过反射设置该字段是容易出错的。如果您使用受保护的只读字段作为软件供应商,则应保留原始字段名称,因为其他任何内容都将迫使您软件的所有用户重新编译甚至更改使用它的代码。无论如何,无法防止访问,但至少可以尽可能地增加难度并使用属性,这也允许您进行某些代码更改。 - Felix K.
不要将变量改为属性并强制所有用户重新编译。 - Felix K.

-2
使用私有字段和受保护的getter有轻微优势:不会引起额外的间接级别。
不要忘记使用C#声明属性的速记方式:
protected int Field { protected get; private set; }

@ZaidMasud 不,私有字段就是 OP 使用的方式。我只是添加了代码来展示简写方式,而不是微观优化 :) - Destrictor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接