对不起,我的标题太含糊了——如果我能想出一个简洁的标题,我就不用问这个问题了。

假设我有一个不可变的列表类型。它有一个操作Foo(x),该操作返回一个新的不可变列表,其中指定的参数作为结尾的额外元素。因此,要建立一个包含"Hello", "immutable", "world"值的字符串列表,你可以这样写:

var empty = new ImmutableList<string>();
var list1 = empty.Foo("Hello");
var list2 = list1.Foo("immutable");
var list3 = list2.Foo("word");

(这是c#代码,我最感兴趣的是c#建议,如果你觉得语言很重要的话。这根本不是一个语言问题,但语言中的习语可能很重要。)

重要的是现有的列表不会被Foo改变,所以是空的。Count仍然返回0。

另一种(更习惯的)达到最终结果的方式是:

var list = new ImmutableList<string>().Foo("Hello")
                                      .Foo("immutable")
                                      .Foo("word");

我的问题是:Foo最好的名字是什么?

编辑3:正如我稍后所揭示的,类型的名称实际上可能不是ImmutableList<T>,这使得位置很清楚。相反,想象它是TestSuite,它是不可变的,因为它所在的整个框架是不可变的……

(编辑3结束)

到目前为止,我想到的选项是:

Add: common in .NET, but implies mutation of the original list Cons: I believe this is the normal name in functional languages, but meaningless to those without experience in such languages Plus: my favourite so far, it doesn't imply mutation to me. Apparently this is also used in Haskell but with slightly different expectations (a Haskell programmer might expect it to add two lists together rather than adding a single value to the other list). With: consistent with some other immutable conventions, but doesn't have quite the same "additionness" to it IMO. And: not very descriptive. Operator overload for + : I really don't like this much; I generally think operators should only be applied to lower level types. I'm willing to be persuaded though!

我选择的标准是:

给出方法调用结果的正确印象(即它是带有额外元素的原始列表) 尽可能清楚地表明它不会改变现有的列表 当像上面的第二个例子一样连接在一起时,听起来很合理

如果我说得不够清楚,请再问我一些细节。

编辑1:以下是我更喜欢Plus而不是Add的理由。考虑以下两行代码:

list.Add(foo);
list.Plus(foo);

在我看来(这是我个人的观点),后者显然是有问题的——它就像写“x + 5;”作为一个单独的语句。第一行看起来是可以的,直到您记得它是不可变的。事实上,加号操作符本身不改变操作数的方式是我最喜欢加号的另一个原因。没有操作符重载的麻烦,它仍然具有相同的含义,其中包括(对我来说)不改变操作数(在本例中是方法目标)。

编辑2:不喜欢添加的原因。

各种各样的回答都很有效:“使用Add。这就是DateTime所做的,而String有Replace方法等,这些方法不会使不可变性变得明显。”我同意,这里有优先权。然而,我见过很多人称之为DateTime。添加或字符串。替换并期待突变。有大量的新闻组问题(如果我仔细研究的话,可能会有这样的问题),这些问题的答案是“您忽略了String.Replace的返回值;字符串是不可变的,返回一个新的字符串"

现在,我应该揭示这个问题的一个微妙之处——类型实际上可能不是一个不可变列表,而是一个不同的不可变类型。特别是,我正在开发一个基准测试框架,在该框架中,您可以向套件中添加测试,并创建一个新的套件。这可能是显而易见的:

var list = new ImmutableList<string>();
list.Add("foo");

不会完成任何事情,但当你把它改成:

var suite = new TestSuite<string, int>();
suite.Add(x => x.Length);

看起来应该没问题。然而,对我来说,这个错误更清楚了:

var suite = new TestSuite<string, int>();
suite.Plus(x => x.Length);

这只是在乞求:

var suite = new TestSuite<string, int>().Plus(x => x.Length);

理想情况下,我希望我的用户不必被告知测试套件是不可变的。我想让他们坠入成功的深渊。这可能是不可能的,但我想试试。

很抱歉,我只讨论了不可变的列表类型,过度简化了最初的问题。并非所有的集合都像ImmutableList<T>:)那样具有自描述性。


当前回答

我有点晚了,但我没有看到一个答案,建议根本不做!让它保持不变。当他们把你的方法放入循环时,你所使用的任何方法都会被滥用,这最终会发生。相反,我建议使用建造器。

MyImmutableBuilder builder = new MyImmutableBuilder(someImmutable);
MyImmutable modifiedImmultable = builder.add(element1).add(element2).build();

在ImmutableList的情况下,你的构建器很可能只是一个List。

I guess it all boils down to... to you really want an easy way to add just a single element to your immutable collection? If it's a common occurrence, you probably don't want to be using an immutable object (you can always call build() any time you want to send a snapshot of the object at it's present state...) If it's not a common occurrence, then having a builder as a requirement to do it wouldn't be a significant impediment, especially if it were documented that this is how you want to build them.

@supercat -考虑你描述的以下场景:

Stack a = someImmutableStackOfSize(10);
Stack b = a.popAndCopy(); // size 9
Stack c = a.popAndCopy(); // also size 9
// a b and c have the same backing list
Stack d = b.pushAndCopy("node1");
Stack e = c.pushAndCopy("node2");
// because d and e had the same backing list, how will it 
// know that d's last element is node1, but e's last
// element is node2?

其他回答

我知道这似乎是在徒劳无益,但我认为您可能会以一种奇怪的方式来处理这个问题(因此命名困难)。当我有一个不可变的对象时,我通常不期望该对象具有暗示可变性的函数(如Add或AddAll)。我知道字符串和DateTime这样做,但即使我已经成为受害者,忘记使用DateTime / String函数的结果,所以我会尽可能避免这些陷阱。

如果我是您,我会遵循类似于Guava使用的Builder模式。参见Guava中的不可变集合。基本上,这个概念是用它所包含的所有内容来构造不可变对象。如果你想要创建一个新的不可变对象,你需要使用一个新的构建器。

var suite = new TestSuite.Builder<string, int>().add(newTest).build();

在用户调用构建之前,您可以扩展构建器以允许对测试套件进行各种修改(如果您愿意,可以使用标准列表命名约定,如添加/删除)。

虽然正确命名方法可以减少误用的机会,但如果有一些语言构造告诉编译器:“这个方法什么都不做,只是返回一个新对象,它应该被使用,否则方法调用是没有意义的”,那么每当有人在错误的设置中使用该方法时,编译器就会报错/警告。

我已经为此填写了一个功能请求,如果你同意,可以自由投票。

对于这种功能,我通常使用动词的第二种形式,在这种情况下是Added。

这个约定被Qt使用,例如QVector2D::normalized返回一个归一化的向量,而QVector2D::normalize则归一化一个向量。

以同样的方式,Added将返回带有新添加项的对象。

也许混淆源于您想要两个操作合一的事实。为什么不把它们分开呢?DSL风格:

var list = new ImmutableList<string>("Hello");
var list2 = list.Copy().With("World!");

Copy会返回一个中间对象,这是原始列表的可变副本。With将返回一个新的不可变列表。

更新:

但是,使用中间的、可变的集合并不是一个好方法。中间对象应该包含在Copy操作中:

var list1 = new ImmutableList<string>("Hello");
var list2 = list1.Copy(list => list.Add("World!"));

现在,Copy操作接受一个委托,该委托接收一个可变列表,因此它可以控制复制结果。它能做的远不止追加一个元素,比如删除元素或对列表排序。它还可以在ImmutableList构造函数中使用,在没有中间不可变列表的情况下组装初始列表。

public ImmutableList<T> Copy(Action<IList<T>> mutate) {
  if (mutate == null) return this;
  var list = new List<T>(this);
  mutate(list);
  return new ImmutableList<T>(list);
}

现在不存在被用户误解的可能性,他们自然会掉进成功的坑里。

还有一个更新:

如果你仍然不喜欢提到可变列表,即使现在包含了它,你可以设计一个规范对象,它将指定或脚本,复制操作将如何转换它的列表。用法是一样的:

var list1 = new ImmutableList<string>("Hello");
// rules is a specification object, that takes commands to run in the copied collection
var list2 = list1.Copy(rules => rules.Append("World!"));

现在你可以对规则名称进行创新,你可以只公开你想要Copy支持的功能,而不是IList的全部功能。

对于链接的使用,你可以创建一个合理的构造函数(当然,它不会使用链接):

public ImmutableList(params T[] elements) ...

...

var list = new ImmutableList<string>("Hello", "immutable", "World");

或者在其他构造函数中使用相同的委托:

var list = new ImmutableList<string>(rules => 
  rules
    .Append("Hello")
    .Append("immutable")
    .Append("World")
);

这里假设规则。Append方法返回这个。

这是你最新的例子的样子:

var suite = new TestSuite<string, int>(x => x.Length);
var otherSuite = suite.Copy(rules => 
  rules
    .Append(x => Int32.Parse(x))
    .Append(x => x.GetHashCode())
);

扩展方法呢?在这种情况下,可以将其称为Join。作为一个扩展方法,用户应该知道它是一个静态方法,因此可能会让他们暂停一下,并鼓励他们查看返回值。同时,您拥有“实例”方法的可用性。

public static ImmutableList<T> Join(this ImmutableList<T> body, T tail)
{
    // add robust error checking in case either is null...
    return new ImmutableList<T>(body, tail);
}

再后来……

var list = new ImmutableList<string>().Join("Hello")
                                      .Join("Extensible")
                                      .Join("World");

我不太清楚发布多个答案的普遍行为,但这是一个有趣的问题,因为我认为命名是设计中的关键步骤,我的大脑一直在思考这个问题。