【问题标题】:Difference between returning reference vs not returning anything返回参考与不返回任何内容之间的区别
【发布时间】:2024-05-01 13:05:02
【问题描述】:

这两种方法有区别吗?

public class A
{
    public int Count { get; set; }
}

public A Increment(A instance)
{
    instance.Count++;
    return instance;
}

public void Increment(A instance)
{
    instance.Count++;
}

我的意思是,除了一个方法返回相同的引用而另一种方法不返回任何内容之外,它们都完成相同的事情,以增加作为参数传递的引用的 Count 属性。

使用一个来对抗另一个有优势吗?由于方法链接,我通常倾向于使用前者,但是否存在性能权衡?

例如,后一种方法的一个优点是不能创建新的引用:

public void Increment(A instance)
{
    instance.Count++;
    instance = new A(); //This new object has local scope, the original reference is not modified
}

这可以被认为是针对接口的新实现的一种防御方法。

我不希望这是基于意见的,所以我明确地从文档或语言规范中寻找具体的优点(或缺点)。

【问题讨论】:

  • 我不明白您所说的无法创建新参考是什么意思。该示例似乎与Increment 是否有返回值无关。
  • 我会说使用public A Increment(A instance) 是引用类型的弊端,除非你正在做cloing(节点之类的东西 - getsiblingnode 等)
  • @ChrisMantle 如果Increment 可以返回A 的一个实例,那么可以执行以下操作:return new A();,并且该方法的使用者最终会得到一个新的引用。具有void 的返回类型可以避免这些情况。
  • 第一个被称为流利设计。它允许像 int count = Increment(instance).Count; 这样的链接,尽管您通常使用相关类型的实例方法来执行此操作,并且它返回对 this 的引用。

标签: c# oop methods


【解决方案1】:

简短回答 - 视情况而定。

长答案 - 如果您使用构建器模式或需要链接方法,我会考虑返回对象的实例。

大多数其他情况看起来确实像代码异味:如果您可以控制 API,并且发现很多地方没有使用返回的对象,那么为什么还要费心费力呢?可能你会创建微妙的错误。

【讨论】:

    【解决方案2】:

    例如,后一种方法的一个优点是不能创建新的引用。

    您可以认为这是缺点之一。考虑:

    public A Increment(A instance)
    {
      return new A { Count = instance.Count +1 };
    }
    

    或者

    public A Increment()
    {
      return new A { Count = this.Count +1 };
    }
    

    始终如一地应用这一点,您可以让您的 A 类不可变,并带来所有优势。

    它还允许返回实现相同接口的不同类型。这就是 Linq 的工作原理:

    Enumerable.Range(0, 1)    // RangeIterator
      .Where(i => i % 2 == 0) // WhereEnumerableIterator<int>
      .Select(i => i.ToString()) // WhereSelectEnumerableIterator<int, string>
      .Where(i => i.Length != 1) // WhereEnumerableIterator<string>
      .ToList();                 // List<string>
    

    虽然每个操作都作用于 IEnumerable&lt;int&gt; 类型,但每个结果都由不同的类型实现。

    如您所建议的那样,变异流畅的方法在 C# 中非常罕见。它们在没有 C# 支持的那种属性的语言中更常见,因为这样做很方便:

    someObject.setHeight(23).setWidth(143).setDepth(10);
    

    但在 C# 中,这样的 setXXX 方法很少见,属性设置器更常见,而且它们不能流利。

    主要的例外是StringBuilder,因为它的本质意味着用不同的值重复调用Append() 和/或Insert() 是很常见的,流畅的风格很适合这种情况。

    否则,变异流利的方法并不常见,这意味着您真正得到的只是返回字段的微小额外成本。它是微小的,但是当与将忽略它的更惯用的 C# 样式一起使用时,它不会获得任何好处。

    拥有一个既发生变异又返回变异对象的外部方法是不寻常的,这可能会导致某人认为您没有变异对象,因为您正在返回结果。

    例如在看到时:

    public static IList<T> SortedList(IList<T> list);
    

    使用该代码的人可能会认为,在调用 list 之后,它会被单独留下,而不是原地排序,而且两者会有所不同,并且可以单独进行变异。

    仅出于这个原因,返回一个新对象或返回 void 以使变异性质更加明显是个好主意。

    虽然我们可以在返回一个新对象时使用捷径:

    public static T[] SortedArray<T>(T[] array)
    {
      if (array.Length == 0) return array;
      T[] newArray = new T[array.Length];
      Array.Copy(array, newArray, array.Length);
      Array.Sort(newArray);
      return newArray;
    }
    

    这里我们利用了一个事实,因为空数组本质上是不可变的(它们没有要变异的元素,也不能添加到其中),对于大多数用途来说,返回相同的数组与返回一个新数组是一样的。 (与 string 如何通过返回 this 实现 ICloneable.Clone() 进行比较)。除了减少完成的工作量外,我们还减少了分配次数,从而减少了 GC 压力。即使在这里我们需要小心(在对象标识上键入集合的人会因此受到阻碍),但它在很多情况下都很有用。

    【讨论】: