如何处理不可变字段上的setter?


25

我有一堂课有两个readonly int领域。它们作为属性公开:

public class Thing
{
    private readonly int _foo, _bar;

    /// <summary> I AM IMMUTABLE. </summary>
    public Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public int Foo { get { return _foo; } set { } }

    public int Bar { get { return _bar; } set { } }
}

但是,这意味着以下代码完全合法:

Thing iThoughtThisWasMutable = new Thing(1, 42);

iThoughtThisWasMutable.Foo = 99;  // <-- Poor, mistaken developer.
                                  //     He surely has bugs now. :-(

假设可以奏效的错误肯定是隐蔽的。当然,错误的开发人员应该已经阅读了文档。但这并不能改变没有编译或运行时错误就此问题警告过他的事实。

应该如何Thing更改类,以使开发人员不太可能犯上述错误?

抛出异常?使用getter方法而不是属性?



1
谢谢,@gnat。那个帖子(无论是问题还是答案)似乎都是在谈论接口,就像在Capital I中一样Interfaces。我不确定这就是我在做什么。
kdbanman

6
@gnat,那是可怕的建议。接口提供了一种很好的方式来维护仍然易于测试的公共不可变VO / DTO。
David Arno

4
@gnat,我做到了,这个问题没有意义,因为OP似乎认为接口会破坏不变性,这是胡说八道。
David Arno

1
@gnat,这个问题是关于声明DTO的接口。投票最多的答案是接口不会有害,但可能不必要。
保罗·德雷珀

Answers:


107

为什么要使该代码合法?如果不采取任何措施,
请取出set { }
这是您定义只读公共属性的方式:

public int Foo { get { return _foo; } }

3
由于某些原因,我认为这不合法,但它似乎可以正常运行! 完美,谢谢。
kdbanman

1
@kdbanman它可能与您在某个时刻看到IDE所做的事情有关-可能是自动完成。
Panzercrisis

2
@kdbanman; 不合法(最高C#5)是public int Foo { get; }(只有吸气剂的自动属性),但是public int Foo { get; private set; }是合法的。
Laurent LA RIZZA

@LaurentLARIZZA,您慢跑了我的记忆。那正是我困惑的来源。
kdbanman 2015年

45

在C#5及更低版本中,对于通过getter公开的不可变字段,我们面临着两种选择:

  1. 创建一个只读的后备变量,然后通过手动获取器将其返回。此选项是安全的(必须明确删除readonly来破坏不变性。尽管如此,它却创建了许多样板代码。
  2. 使用带有私有设置器的自动属性。这样可以创建更简单的代码,但是更容易意外破坏不变性。

但是,使用C#6(在昨天发布的VS2015中可用),我们现在可以两全其美:只读自动属性。这使我们可以将OP的类简化为:

public class Thing
{
    /// <summary> I AM IMMUTABLE. </summary>
    public Thing(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public int Foo { get; }
    public int Bar { get; }
}

Foo = fooBar = bar线仅在构造(其实现了鲁棒只读要求)和背场是隐含的,而不是必须明确定义(其实现了更简单的代码)有效。


2
通过消除构造函数的语法开销,主构造函数可以进一步简化此过程。
塞巴斯蒂安·雷德尔


1
@DanLyons该死,我期待在整个代码库中使用它。
塞巴斯蒂安·雷德尔

12

您可以摆脱二传手。他们什么都不做,会迷惑用户,并导致错误。但是,您可以改为将它们设为私有,从而摆脱后备变量,从而简化代码:

public class Thing
{
    public Thing(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public int Foo { get; private set; }

    public int Bar { get; private set; }
}

1
public int Foo { get; private set; }“该类不再是不变的,因为它可以更改自身。不过谢谢!
kdbanman

4
所描述的类是不可变的,因为它本身不会更改。
David Arno

1
我的实际课堂比我给的MWE还要复杂。我追求真正的不变性,并具有线程安全性和类内部保证。
kdbanman

4
@kdbanman,您的意思是?如果您的类仅在构造过程中写给私有设置者,则它实现了“真正的不变性”。该readonly关键字只是一个防差错,也就是说,它扼守反对阶级打破在未来的不可变性; 但是并不需要实现不变性。
David Arno

3
有趣的术语,我必须在Google上搜索-poka-yoke是日语术语,表示“防错”。你是对的!那正是我在做的。
kdbanman

6

两种解决方案。

简单:

请勿包含David指出的不可变只读对象的设置器。

或者:

允许设置者返回一个新的不可变对象,该对象比前一个更为冗长,但会为每个初始化的对象提供一段时间的状态。对于线程安全和不变性而言,此设计是非常有用的工具,它扩展到所有命令式OOP语言。

伪码

public class Thing
{

{readonly vars}

    public Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public Thing withFoo(int foo) {
        return new Thing(foo, getBar());
    }

    public Thing withBar(int bar) {
        return new Thing(getFoo(), bar)
    }

    etc...
}

公共静态工厂

public class Thing
{

{readonly vars}

    private Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public static with(int foo, int bar) {
        return new Thing(foo, bar)
    }

    public Thing withFoo(int foo) {
        return new Thing(foo, getBar());
    }

    public Thing withBar(int bar) {
        return new Thing(getFoo(), bar)
    }

    etc...
}

2
setXYZ是工厂方法的可怕名称,请考虑使用XYZ等。
Ben Voigt,2015年

谢谢,更新了我的答案以反映您的有用评论。:-)
马特·史密西斯

问题是专门询问属性设置器,而不是设置器方法。
user253751

的确如此,但OP担心未来可能的开发人员会出现可变状态。使用private readonly或private final关键字的变量应该是自记录的,如果不理解,那不是原始开发人员的错。了解改变状态的不同方法是关键。我添加了另一种非常有用的方法。代码世界中有很多事情会导致过度的妄想症,私有只读变量不应该是其中之一。
马特·史密西斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.