说,我有一个班级:
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
中保留它。
有没有办法实现这个目标?
答案 0 :(得分:6)
你不应该期望ms[0]
改变。初始化后,fromList
与ms
完全无关 - fromList
和ms[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);
这就是为什么这个例子有效的原因。它替换了列表中存储的对象。这是你不工作的例子中缺少的一步。