【问题标题】:How to make += operator keep the object reference?如何使 += 运算符保留对象引用?
【发布时间】:2010-02-19 13:52:12
【问题描述】:

说,我有课:

class M
{
     public int val; 

其中还有一个+ 运算符:

    public static M operator +(M a, M b)
    {
        M c = new M();
        c.val = a.val + b.val;
        return c;
    }
}

我有一个 List 的类对象:

List<M> ms = new List();
M obj = new M();
obj.val = 5;
ms.Add(obj);

其他一些对象:

M addie = new M();
addie.val = 3;

我可以这样做:

ms[0] += addie;

它确实可以按我的预期工作 - 列表中的值已更改。但是如果我想做

M fromList = ms[0];
fromList += addie;

由于显而易见的原因,它不会更改 ms 中的值。

但直觉上我希望ms[0] 在那之后也会发生变化。真的,我从列表中选择对象,然后用其他对象增加它的值。所以,由于我在添加之前在fromList 中保存了对ms[0] 的引用,所以我希望在执行后仍将它保存在fromList 中。

有什么方法可以实现吗?

【问题讨论】:

  • 这里的 fromList 不应该只是某种参考吗?我对 C# 的了解还不够,无法告诉你如何操作。
  • ms[0] = ms[0] += addie; 是我认为的普通代码

标签: c# operators addition


【解决方案1】:

您不应该期望ms[0] 改变。在它被初始化之后,fromList 根本没有与ms 连接——fromList 和ms[0] 恰好具有相同的值,但仅此而已。 += 正在返回一个新值(确实应该如此),因此您只是更改存储在fromList 中的值,这完全独立于列表中的值。 (注意这里的值是引用,不是对象。)

不要试图改变这种行为——它在做正确的事。改变你的期望。

如果您真的希望列表的内容反映更改,您要么需要更改列表中的值以引用新对象,要么需要更改代码以进行变异现有对象而不是创建新对象。如果您采用后一种方法,您应该不在运算符中执行此操作。相反,创建一个Add 方法,这样你就可以调用:

fromList.Add(addie);

很明显,这是一个变异操作,所以它不会破坏任何期望。

我个人会尝试使用不可变类型,并调整您的设计,以便您不需要更改列表(或者您直接对列表进行操作)。

【讨论】:

  • 虽然这是真的,但 C# 中是否没有指针或引用来解决此类问题?
  • +1,使用运算符重载来巧妙地改变运算符的含义通常是一个非常糟糕的主意。
  • @roe:有参考资料,但不是你想的 IMO 那种。列表和变量都将引用对象 - 对象不直接存储在列表中。
  • 我在想 C++ 参考:M &amp;fromList = ms[0];
  • 我要对 Skeet 投反对票?我对以下陈述有异议:“在初始化之后,fromList 根本与 ms 无关。”实际上,fromList 和 ms 都引用了同一个 M 实例。“你只是在更改存储在 fromList 中的值,它完全独立于列表中的值。” fromList 和 list 都不保存值,M 是引用类型,fromList 和 ms[0] 都保存引用。当然,您是正确的 operator+= 不会更新左侧引用的实例,它会使 LHS 引用一个新实例。想想 VB6 让/设置。
【解决方案2】:

您将可变行为与不可变行为混合在一起,我认为这就是令人困惑的地方。

对象是可变的(即您可以更改它的属性),并且当您为其属性赋值时,它的行为与您所期望的一样。

当您使用 + 运算符时,它的行为相当于一个不可变的值。假设您有一个整数列表,并将一个整数读入一个变量。如果您更改了变量的值,则不会更改列表中的整数:

List<int> list = new List<int>() { 1, 2, 3};
int x = list[0];

// this will of cousre not change the content in the list:
x += 42;

当你有这个代码时:

M fromList = ms[0];
fromList += addie;

编译器像这样使用 + 运算符:

M fromList = ms[0];
fromlist = fromList + addie;

表达式fromlist + addie 返回一个对类的新实例的引用,并将该引用分配给变量。这自然不会改变变量之前引用的对象。

【讨论】:

    【解决方案3】:

    尽量避免实现运算符。大多数时候它不值得麻烦,并使代码更加混乱。改为实现简单的加/减方法。此外,如果您最终在对象中添加其他值,则扩展将更容易。通过使用运算符,您可以使用与整个对象相关的语义来作用于它的一部分。与 struct 一起使用时,运算符通常更有意义。在实现应该表现得像值类型的类型时,应该使用结构体。

    【讨论】:

    • 如果您的对象可以添加/减去,例如如果它表示行为类似于数字的东西,那么重载运算符非常有意义。此外,如果您有一些行为类似于索引容器的东西,那么提供索引器实现非常有意义,有效地重载了[] 运算符。这适用于类与(如果不超过)结构一样多。
    【解决方案4】:

    真正的答案是他的 + 运算符没有在 lhs 对象上进行操作,它制作了一个副本并增加了副本。如果他的 + 运算符实际上更改了对象,那么它将更改列表中的值。

    之后

     M fromlist = ms[0];
    

    fromlist 和 ms[0] 都指向同一个对象。现在

     fromlist += addie;
    

    确实如此

     fromlist = fromlist.op+(addie);
    

    op+ 用 val = fromlist.val + addie.val 返回一个新的 M

    所以现在 fromlist 和 ms[0] 指向具有不同值的不同事物

    【讨论】:

      【解决方案5】:

      这是我在其他答案中没有看到的不同角度。

      在 C++ 中,创建类数组类的方法是重载 [](称为下标运算符):

      std::string operator[](int nIndex);
      

      但该定义仅允许您阅读项目。为了支持写作,您需要返回我们 C++ 粉丝很快就会开始调用 lvalue reference 的内容:

      std::string& operator[](int nIndex);
      

      这意味着(至少在这种情况下)它可以出现在赋值表达式的左侧:

      list[3] = "hi";
      

      但它会引发各种可怕的终生问题。如果你能得到这样的引用,你可以把它绑定到一个名字上:

      std::string& oops = list[3];
      

      现在oops 指的是list 内部的东西。这不是一个非常安全的情况,因为对list 的某些操作会导致oops 成为定时炸弹,您需要详细了解list 如何工作以避免这种情况。

      C# 的工作方式不同。类数组类的设计者可以为 [] 运算符定义两个单独的定义,作为 indexer(下标运算符实现的 C# 名称)的一部分:

      public string this[int index]
      {
          get { /* return a string somehow */ }
          set { /* do something with implicit 'value' param */ }
      }
      

      这巧妙地绕过了对左值引用的需求。这也意味着您的简单示例实际上涉及一些漂亮的编译器步法:

      ms[0] += addie;
      

      正如许多其他答案所说,这是使用普通的+ 运算符实现的,因此它扩展为:

      ms[0] = ms[0] + addie;
      

      但是ms[0] 的这两次出现实际上是对完全不同的方法的调用。有点像:

      ms.SetAt(0, ms.GetAt(0) + addie);
      

      这就是该示例有效的原因。它替换存储在列表中的对象。这是您的无效示例中缺少的步骤。

      【讨论】:

        【解决方案6】:

        复合赋值和简单运算符之间的关系在 C# 中与在 C++ 中完全不同。

        在 C# 中,复合赋值运算符调用简单运算符,该运算符将结果放入新实例中,然后更改原始变量以引用新实例。原始引用不受影响。

        在 C++ 中,复合赋值运算符会改变所指对象。在大多数情况下,简单的算术运算符首先将 LHS 克隆到一个临时对象中,然后调用复合赋值运算符,对临时对象进行变异。

        所以简单运算符在两者中的工作方式相同,产生一个新实例,但复合赋值运算符总是在 C# 中创建一个新实例,而不是在 C++ 中。

        【讨论】:

        • 不同的不仅仅是 compound 分配。简单赋值也有很大不同,因为 C++ 对象具有复制语义(如 C# 结构),而 C# 更喜欢引用语义。
        • @dan:我的意思是说“简单算术”运算符,而不是“简单赋值”。现在修好了。您关于简单赋值的陈述仅适用于您暗示的引用类型。当然,引用类型很像 C++ 指针(但除了分配时会自动取消引用)。
        猜你喜欢
        • 2023-01-12
        • 2017-12-07
        • 1970-01-01
        • 1970-01-01
        • 2021-04-27
        • 1970-01-01
        • 1970-01-01
        • 2022-12-21
        • 1970-01-01
        相关资源
        最近更新 更多