1

说,我有一堂课:

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]in的引用fromList,所以我希望在执行后仍然持有fromList它。

有什么方法可以实现吗?

4

6 回答 6

6

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

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

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

fromList.Add(addie);

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

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

于 2010-02-19T13:55:50.463 回答
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返回对类的新实例的引用,并将该引用分配给变量。这自然不会改变变量之前引用的对象。

于 2010-02-19T14:26:25.610 回答
0

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

于 2010-02-19T14:05:46.437 回答
0

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

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

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

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

于 2010-02-22T21:59:46.553 回答
0

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

 M fromlist = ms[0];

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

 fromlist += addie;

实际上确实

 fromlist = fromlist.op+(addie);

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

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

于 2010-02-22T22:08:24.220 回答
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);

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

于 2010-02-22T22:48:41.313 回答