C#,不可变性和公共只读字段

35

我在很多地方都看到过公开字段不是个好主意,因为如果以后想要改成属性,你必须重新编译使用你的类的所有代码。

然而,在不可变类的情况下,我不明白为什么你需要改成属性 - 毕竟你不会添加“set”中的逻辑。

对此有任何想法吗?我有什么遗漏吗?

以下是针对那些更容易阅读代码而非文本的人的区别示例:

//Immutable Tuple using public readonly fields
public class Tuple<T1,T2>
{
     public readonly T1 Item1;
     public readonly T2 Item2;
     public Tuple(T1 item1, T2 item2)
     {
         Item1 = item1;
         Item2 = item2;
     }
}

//Immutable Tuple using public properties and private readonly fields
public class Tuple<T1,T2>
{
     private readonly T1 _Item1;
     private readonly T2 _Item2;
     public Tuple(T1 item1, T2 item2)
     {
         _Item1 = item1;
         _Item2 = item2;
     }
     public T1 Item1 { get { return _Item1; } }
     public T2 Item2 { get { return _Item2; } } 
}

当然,你可以使用自动属性(public T1 Item1 { get; private set; }),但这仅仅能够获得“约定的不可变性”而不是“保证的不可变性”...

6个回答

12

C# 6.0现在支持自动属性初始化程序。

自动属性初始化程序允许直接在其声明中分配属性。对于只读属性,它会处理所有必需的程序以确保属性是不可变的。

您可以在构造函数或使用自动初始化程序中初始化只读属性。

public class Customer
{
    public Customer3(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
    public string FirstName { get; }
    public string LastName { get; }
    public string Company { get; } = "Microsoft";
}

var customer = new Customer("Bill", "Gates");

您可以在这里了解有关自动属性初始化器的更多信息。


11

在属性中很明显你不能像下面这样写:

public T2 Item2 { get; readonly set; } 

我甚至不确定readonly是用来表示“只能在构造函数中设置”的最佳词语,但这就是我们所束缚的。

实际上,很多人都要求这个功能,希望它能在C#的某个假想新版本中引入。

请参见相关问题


2
嗯,一个“假想的新版本C#”我想我知道你在读谁的博客了 ;) - Benjol
1
是的,我们正在考虑。假设性地说,这实际上比一开始看起来要困难得多。 - Eric Lippert
2
@Eric Lippert,你现在只是在逗我们玩。我期待着不久之后能看到一篇关于这些困难的文章 :) - Benjol
1
@Benjol:棘手的部分在于,希望将这种属性的初始化看起来像匿名类型的初始化或对象初始化器的初始化。但是在前者中,我们生成构造函数,在后者中,属性在构造之后更改。如何使所有这些合理化是不明显的。 - Eric Lippert
@Eric,有没有办法在那里放置 Naj? :) - Benjol
我将这个标记为答案,尽管它并没有直接回答我的问题... - Benjol

4
您将来可能不需要向setter添加任何逻辑,但您可能需要向getter添加逻辑。这是我选择使用属性而不是暴露字段的足够好的理由。如果我感到严格,我会选择完全不可变性(明确的只读支持字段,暴露getter并且没有setter)。如果我感到懒惰,我可能会选择“约定不可变性”(自动属性,暴露getter和私有setter)。

1
在C#9中,我们有init访问器可以替代set访问器。
var firstCar = new Car { Color = "Orange", Brand = "Mclaren" };
        
public class Car
{
    public string Color { get; init; }
                
    public string Brand { get; init; }
}

使用init访问器代替set访问器声明了一个仅初始化属性(或索引器)

包含init访问器的实例属性在以下情况下被认为是可设置的,除了在局部函数或lambda中:

  • 在对象初始化程序期间
  • 在with表达式初始化程序期间
  • 在包含类型或派生类型的实例构造函数中,在this或base上
  • 在任何属性的init访问器中,在this或base上
  • 在具有命名参数的属性用法中

Init only setters


0

作为标准惯例,我只遵循您的第二个示例,仅在对象是公共的或易于意外篡改时使用“readonly”。我正在使用“约定不可变性”模型构建插件框架的当前项目。显然,在约定不可变性下,readonly保护被移除。

只有在极少数情况下,我才会暴露一个字段 - 公共的、内部的或其他的。除非编写属性{get;}需要更多时间,否则这种做法就不太合适。


0
属性的概念是,即使您现在或将来不打算更改它们,也可能需要以某种意想不到的方式进行更改。假设您需要更改getter以执行某种计算或记录。也许您需要添加异常处理。有很多潜在原因。
还要考虑语义学。如果T1是值类型而不是引用类型,则访问obj.Item1会返回getter中_Item1的副本,而没有getter的Item1不会检索副本。这意味着,虽然Item1在内部可能是不可变的,但返回的值类型对象并非如此。我想不出为什么这会是一件好事,但这确实是一个区别。

通过属性返回时复制是什么意思?你有相关的参考资料吗? - Benjol
当你传递值类型时,它们必须被复制。这是C#的工作方式。 - Erik Funkenbusch
1
是的,但是为什么传递时该字段不会被复制呢?我看不出这会有什么问题。 - Benjol
如果您传递的是 obj.field.ToString(),它将在原始实例上运行。如果它是值类型,则 obj.property.ToString() 将在副本上运行。 - Erik Funkenbusch

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