如何使+ =运算符保持对象引用?

时间:2010-02-19 13:52:12

标签: c# operators addition

说,我有一个班级:

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]在此之后也会发生变化。真的,我从列表中选择对象,然后用其他对象增加它的值。因此,由于我在添加之前在ms[0]中引用了fromList,因此我希望在执行之后仍然可以在fromList中保留它。

有没有办法实现这个目标?

6 个答案:

答案 0 :(得分:6)

不应该期望ms[0]改变。初始化后,fromListms完全无关 - fromListms[0]碰巧具有相同的值,但这就是全部。 + =正在返回一个新值(实际上应该如此),因此您只需更改fromList中存储的值,该值完全独立于列表中的值。 (请注意,这里的值是引用,不是对象。)

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

如果确实希望列表的内容反映更改,则需要更改列表中的值以引用新对象,或者需要将代码更改为mutate现有对象而不是创建新对象。如果采用后一种方法,您应该在运营商中执行此操作。相反,请创建一个Add方法,以便您调用:

fromList.Add(addie);

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

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

答案 1 :(得分:4)

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

该对象是可变的(即您可以更改它的属性),并且它的行为与您为其属性赋值时的预期相同。

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

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返回对类的新实例的引用,并将该引用赋给该变量。这自然不会改变变量之前引用的对象。

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

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

在C ++中,复合赋值运算符会改变引用。在大多数情况下,简单算术运算符首先将LHS克隆为临时值,然后调用复合赋值运算符,对临时值进行变换。

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

答案 4 :(得分:0)

真正的答案是他的+操作符不在lhs对象上操作,它会复制并增加副本。如果他的+运算符实际上改变了对象,那么它将改变列表中的值。

 M fromlist = ms[0];

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

 fromlist += addie;

实际上

 fromlist = fromlist.op+(addie);

op +返回一个新的M,其中val = fromlist.val + addie.val

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

答案 5 :(得分:0)

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

在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#的工作方式不同。类数组类的设计者可以为[]运算符定义两个单独的定义,作为索引器的一部分(下标运算符的实现的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);

这就是为什么这个例子有效的原因。它替换了列表中存储的对象。这是你不工作的例子中缺少的一步。