如何实现只读属性

116

我需要在我的类型上实现一个只读属性。此外,这个属性的值将在构造函数中设置并且不会改变(我正在编写一个为WPF公开自定义路由UI命令的类,但这并不重要)。

我看到两种做法:

  1. ...

class MyClass
{
    public readonly object MyProperty = new object();
}
  • class MyClass
    {
        private readonly object my_property = new object();
        public object MyProperty { get { return my_property; } }
    }
    
  • 所有这些 FxCop 错误都说我不应该有公共成员变量,看起来第二个方法是正确的。这是正确的吗?

    在这种情况下,只有属性和只读成员之间有什么区别吗?


    45
    有时我希望自动属性语法包括一个 get; readonly set; 选项。 - Dan Bryant
    可能是C#,不可变性和公共只读字段的重复问题。 - user
    3
    @DanBryant 这里至少有 get; private set; - Marc.2377
    3
    @Marc.2377,实际上,他们不久前添加了对{get;}的支持,这解决了这个问题。 - Dan Bryant
    @DanBryant 啊,确实。我应该先读答案的 ;) - Marc.2377
    8个回答

    83

    C# 6.0增加了只读自动属性

    public object MyProperty { get; }
    
    当您不需要支持旧编译器时,使用只读属性可以使代码与只读字段一样简洁。如果您只关心源代码兼容性,则版本差异并不重要。但是,为了实现二进制兼容性,使用属性更好,因为您可以将其替换为一个具有setter的属性,而不会破坏依赖于您的库的已编译代码。在这种情况下,遵循惯例是更好的选择。其中一个例外是反射代码,它可能仅接受属性而不是字段,例如属性编辑器/查看器。从字段更改为属性可能会破坏许多序列化程序。 XmlSerializer仅序列化公共属性而不是公共字段。另一种常见的变体是使用私有setter的自动属性。虽然这很短且是一个属性,但它并未强制执行只读性。因此,我更喜欢其他方法。只读字段本身说明书写清楚,即使反射也无法更改,这是其优点之一。但说实话,在应用程序代码中,我经常使用第一种方式,因为我很懒。在库中,我通常更加详细并遵循约定。

    1
    目前我希望C# 9添加public type prop => get; - NetMage
    但是getter-only属性和readonly属性是完全不同的。 只读类字段通常用于在类构造期间初始化的变量,并且以后永远不会更改。 简而言之,如果您需要确保属性值从外部永远不会被更改,但需要能够从类代码内部更改它,请使用“仅获取”属性。 - Commander

    82
    第二种方法是首选的选项。
    private readonly int MyVal = 5;
    
    public int MyProp { get { return MyVal;}  }
    

    这将确保MyVal只能在初始化时分配(也可以在构造函数中设置)。

    正如您所指出的那样-这种方法不会暴露内部成员,从而允许您在将来更改内部实现。


    谢谢。第一个选项也可以确保相同的结果。你认为为什么这个是首选的选项呢? - akonsu
    有没有任何理由我们不应该使用 public int MyProp { get; private set; }?我知道它并不是真正的只读,但它非常接近。 - Mike Hofer
    3
    @Mike Hofer - 因为 int 声明为 _readonly_,所以在构造函数外部无法更改它。 - Oded
    @Oded:没错,但他问的是如何实现只读属性,而不是只读字段。我一直认为只读属性的正确实现方式是我展示的那种方式,但我总是愿意学习新东西。我承认,我提供的实现只从实现类外部代码的角度来看是只读的,因此(在这方面)它只是半只读的。也许这就是你展示的实现方式成为首选实现方式的原因?如果是这样,那么这是否会使自动属性的实用性大打折扣呢? - Mike Hofer
    6
    @Mike Hofer - 这实际上取决于意图是什么... 我写的版本公开了一个内部成员,其值在初始化后无法更改。你的公开了一个成员,其值在初始化后可能会更改。这实际上取决于您想要什么... 我的是只读的,也就是说,在初始化后完全不能更改,而你的是只读的,也就是说,由任何外部类更改不了。 - Oded
    1
    @Oded - 我能接受这个观点。这是一个微妙但重要的区别。我可以理解如果你希望在内部和外部都以常量的方式暴露一个属性,这将会非常有用。而我的实现显然无法做到这一点。 - Mike Hofer

    60

    自C# 6(在VS 2015中)引入后,您现在可以拥有只读自动属性,其中隐式后备字段是readonly的(即值可以在构造函数中分配,但不能在其他地方分配):

    public string Name { get; }
    
    public Customer(string name)  // Constructor
    {
        Name = name;
    }
    
    private void SomeFunction()
    {
        Name = "Something Else";  // Compile-time error
    }
    

    现在,您还可以内联初始化属性(带或不带setter):

    public string Name { get; } = "Boris";
    

    回到这个问题,选择第二种方式的优点在于公共成员是一个属性而不是字段,同时又具备第一种方式的简洁性。

    不幸的是,在公共接口的层面上它并不能保证不可变性(正如@CodesInChaos所指出的关于自我文档化的问题),因为对于类的消费者来说,没有setter和有私有setter是无法区分的。


    新语法提供了很好的信息,但是你可以使用反射激活私有 setter 和/或将值加载到后备字段中(不考虑访问修饰符)。此外,也可以使用反射在运行时区分私有 setter 和缺少 setter。 - Shaun Wilson
    1
    @Shaun:说得好 - 反射可以做很多事情!许多可能性可能违背了原始程序员甚至语言设计者的意图,但你是在说使用反射可以更改readonly字段吗(我不知道答案,但这似乎有问题)? - Bob Sammers
    2
    我并没有特别地说那个,但答案是:可以。https://gist.github.com/wilson0x4d/a053c0fd57892d357b2c 如果你认为这有问题,那么等你了解到地球上的每个操作系统都有一个机制,其中一个进程可以读/写任何其他进程的内存(如果具有足够的执行权限),你就会更觉得问题严重了。这就是为什么没有基于软件的系统可以真正安全的原因,但是,我离题了,这与原始问题没有太大关系 :) 即使它很有趣! - Shaun Wilson
    我建议您也阅读详细的文章BillWagner的文章 - hastrb

    25
    在C# 9中,微软引入了一种新的方式来使用init访问器仅在初始化时设置属性,如下所示: (链接)
    public class Person
    {
      public string FirstName { get; init; }
      public string LastName { get; init; }
    }
    

    这样,您可以在初始化新对象时分配值:

    var person = new Person
    {
      Firstname = "John",
      LastName = "Doe"
    }
    

    但是后来,你无法更改它:

    person.LastName = "Denver"; // throws a compiler error
    

    2
    init 属性的后备字段也是 readonly 的。 - Dealdiane
    你需要使用.NET 5或更高版本,而不是.NET Framework。 - Dave Doknjas

    13

    你可以这样做:

    public int Property { get { ... } private set { ... } }
    

    8
    可以,但是使用这种技术只能保证类的消费者无法修改该属性,不能保证该属性在对象生命周期内始终保持不变。 - Bob Sammers
    Bob:如果类的使用者无法修改属性,那么该属性就是“只读(readOnly)”的,不是吗? - El Bayames
    @ElBayames 你不能改变引用,但可以改变内部。考虑当你暴露一个带有内部状态的对象时。你可以轻松地使用公开的get方法检索对象,然后更改该对象的内部属性。这不是真正的只读。 - Matthew S

    5

    我认为第二种方式更好。这个偏好的唯一真正原因是.NET类不应该有公共字段的普遍偏好。然而,如果该字段是只读的,我看不出除了与其他属性的一致性缺乏外还会有任何真正的反对意见。只读字段和只读属性之间的真正区别在于只读字段提供了一个保证,即其值在对象的生命周期内不会改变,而只读属性则没有。


    5

    4
    这段代码无法简化为一行:public int MyProp { get; } = 5; - Cfun

    4
    第二种方法更受欢迎,因为它具有封装性。你当然可以将readonly字段设置为public,但这违反了C#的习惯用法,其中数据访问是通过属性而不是字段进行的。
    这样做的原因是属性定义了公共接口,如果支持该属性的实现发生变化,你不会破坏其余代码,因为实现被隐藏在接口后面。

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