2

我一直在想这个问题,并且无法找到关于该主题的太多评论,也无法自己得出结论。

创建对象时,公认的最佳做法是尽可能限制对象的公开数据和允许操作该数据。如果您可以使对象不可变,尤其是在多线程应用程序中,那么最好这样做。

话虽如此,C# 似乎偏爱不遵循此规则的开发人员,允许类的定义更简单,更重要的是,更易于维护。

采用以下只读不可变类:

public class ActiveDirectoryUser
{
    private readonly string firstName, lastName, office, username, email;

    public ActiveDirectoryUser(string firstName, string lastName, string office, string username, string email)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.office = office;
        this.username = username;
        this.email = email;
    }

    public string FirstName
    {
        get { return firstName; }
    }

    public string LastName
    {
        get { return lastName; }
    }

    ...

    public string Email
    {
        get { return email; }
    }
}

并将其与下面更简单的示例进行比较,该示例不是只读的。

public class ActiveDirectoryUser
{
    public string FirstName
    {
        get;
        set;
    }

    public string LastName
    {
        get;
        set;
    }

    ...

    public string Email
    {
        get;
        set;
    }
}

可以用以下实例化的是:

ActiveDirectoryUser user = 
    new ActiveDirectoryUser
       { FirstName= "Sam", LastName = "Shiles", ..., Email ="Sam@shiles.com" };

给定更简单的定义、更少的代码行、更少的开发人员错误机会以及其他开发人员可以轻松理解代码(尤其是比我们的简单示例更真实的示例),这就是创建正确的价值只读的,不可变的对象,值得付出代价吗?

此外,其他人是否认为在 C# 中制作不可变对象应该更容易使用自动 esq 语法,例如:

public string FirstName {get; readonly set;}
4

4 回答 4

8

给定更简单的定义、更少的代码行、更少的开发人员错误机会以及其他开发人员可以轻松理解代码(尤其是比我们的简单示例更真实的示例),这就是创建正确的价值只读的,不可变的对象,值得付出代价吗?

是的。

我注意到没有什么能阻止你说

int Foo { get; private set; }

然后只在构造函数中使用setter,以保证属性的不变性。你只是没有得到好的对象初始化语法,但你可以很容易地创建一个构造函数。

此外,其他人是否认为使用自动 esq 语法在 C# 中制作不可变对象应该更容易......

是的。C# 语言设计团队尤其相信这一点。

C# 3.0 添加了许多特性,使 C# 成为一种更“不可变友好”的语言:匿名类型、查询理解和表达式树尤其鼓励不可变数据的“函数式”编程风格。它还添加了使 C# 成为更“可变友好”语言的功能:自动属性、对象初始化器和类型推断数组浮现在脑海中。

语言设计团队很清楚,在 DLR 团队正在制作一个庞大的不可变表达式树类型库的同时,后面的这些特性使创建可变对象变得更加容易。我可以向你保证,他们并没有失去讽刺意味。

语言设计团队周围有许多提议,以寻求更好的语法糖来制作不可变对象。需要可变性的对象初始值设定项具有与不可变的匿名类型初始值设定项几乎相同的语法这一事实显然是探索的起点,但不是唯一的起点。

当然,所有这一切,首先,我不再代表语言设计团队发言,其次,除了 Roslyn 项目之外,没有公布的 C# 未来版本。因此,关于假设的未来语言版本的可能特性集的猜测只是:我的猜测。

于 2013-01-21T16:26:24.833 回答
3

创建适当的只读、不可变对象的价值是否值得付出代价?

这取决于您对不变性、线程安全性的关注程度以及您对其他开发人员不会犯错误的信任程度。

我会问相反的问题 - 可能的线程错误的成本是否值得真正快速而简单地创建可变对象的价值?

于 2013-01-21T13:06:58.757 回答
1

当我思考这些问题时,我倾向于将代码分为几类。

  • 应用层
  • 内部框架层
  • 公共 API 级别

在应用程序级别,您的类通常是叶类,旨在实现项目范围内的非常特定的功能。它们很少被重用,唯一依赖于它们的是应用程序本身中的其他特定类。

在这里,我给自己一些自由,我想说一些公共财产不会伤害任何人。无论如何,这一切都非常本地化,并且随着应用程序的更改,您仍然需要了解代码如何交互才能对此代码进行任何更改。然而,自由不等于草率。

在框架级别上,我的意思是在项目之间高度可重用的子系统。可能是多年来已被证明有效的盐+哈希密码类。或者可能是您在多个项目中依赖的整个第三方产品的包装器。

引用此类程序集或将源文件添加到解决方案后,您的应用程序仍可被视为该代码的唯一使用者。但是该代码仍然必须非常包含,并且应该承诺遵守合同。如果公共属性允许对象具有损坏状态,我会说这是一个非常蹩脚的框架。

在 Api 级别上,情况发生了巨大变化。现在您正在设计的代码应该会随着时间的推移被许多订阅者使用。只是不要与术语混淆,这里明显的候选者是 .NET Framework 本身。但是,如果您是提供 api 的人,人们甚至可以通过保修付费;你真的想要一个不会轻易被滥用的稳定系统。

我认为有趣(并且有点有趣)的是,来自 OOP 纯粹主义者和 SOLID 纯粹主义者(以及其他纯粹主义者)的所有好的原则似乎都默认假设他们的所有代码都在高端支持中api 级别。

所以为了回答你的问题,我会给你几个问题来思考

  • 这个对象会改变状态吗?
  • 是否会更改其他代码段的破坏假设?
  • 如果是这样,是否会以这种方式滥用我的对象的任何理智使用?
  • 如果我的对象被疯狂滥用,这是本地错误还是主要支持问题?
  • 我在这里实际需要什么级别的保护?
  • 我要为此付出多少努力?
  • 这是否带有任何文档?

举一个明确的例子。在我引以为豪的 Web 项目中,很多数据都缓存在单例中(实际上是静态类)。我们确实正确锁定以确保 GetOrSet 上的线程安全。除了一个小细节。我们将缓存中实际对象的引用分发给多个线程。它们是可变的,任何写入其公共属性的尝试都可能导致各种问题。现在我们克隆它们吗?让它们不可变?复制这些对象会降低性能,这样它们就不会造成伤害?没有。由于我们是自己缓存的唯一消费者,我们只是决定将来自服务的所有数据都视为只读。当我们确实需要副本时,我们会使用 Automapper。我们会在代码审查期间对此进行检查。它工作得很好。虽然我们有一些错误,但还没有一个是由写入可变对象引起的。

病人: - 我这样跳的时候腿疼。

医生:-那就不要那样跳了!

于 2013-01-21T14:46:15.210 回答
0

尽管您的第二个示例不正确(它不是不可变的,您应该添加一个构造函数,该构造函数接受与您的字段匹配的参数)我会说它可以是因为 readonly 修饰符表示您在编写类时的意图。这意味着将来当其他人修改类时,他们将知道这些字段是只读的,而不是它们实际上是只读的。

于 2013-01-21T13:09:00.610 回答