views:

220

answers:

6

Say, I have a class:

class M
{
     public int val; 

And also a + operator inside it:

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

And I've got a List of the objects of the class:

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

Some other object:

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

I can do this:

ms[0] += addie;

and it surely works as I expect - the value in the list is changed. But if I want to do

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

it doesn't change the value in ms for obvious reasons.

But intuitively I expect ms[0] to also change after that. Really, I pick the object from the list and then I increase it's value with some other object. So, since I held a reference to ms[0] in fromList before addition, I want still to hold it in fromList after performing it.

Are there any ways to achieve that?

+5  A: 

You shouldn't expect ms[0] to change. After it's been initialized ,fromList isn't connected with ms at all - fromList and ms[0] happen to have the same value, but that's all. += is returning a new value (as indeed it should) so you're just changing the value stored in fromList, which is completely independent of the value in the list. (Note that the values here are references, not objects.)

Don't try to change this behaviour - it's doing the right thing. Change your expectations instead.

If you really want the contents of the list to reflect the change, you either need to change the value in the list to refer to the new object, or you need to change your code to mutate the existing object instead of creating a new one. If you take the latter approach you should not do this within an operator. Instead, create an Add method, so you'd call:

fromList.Add(addie);

It's fairly clear that that's a mutating operation, so it won't break any expectations.

Personally I'd try to use immutable types instead, and adjust your design so that you don't need the list to change (or you operate on the list directly).

Jon Skeet
while this is true, are there no pointers or references in C# to solve that type of problem?
roe
+1, using operator overloading to subtly change the meaning of an operator is generally a very bad idea.
John M Gant
@roe: There are references, but not of the kind you're thinking of IMO. Both the list and the variable will have a reference to the object - the object isn't stored in the list directly.
Jon Skeet
I'm thinking of the C++ reference: `M `
roe
I get to downvote Skeet? I'm taking issue with the following statements: "After it's been initialized, fromList isn't connected with ms at all." Actually, fromList and ms both reference the same instance of M. "you're just changing the value stored in fromList, which is completely independent of the value in the list." Neither fromList nor the list hold values, M is a reference type and fromList and ms[0] both hold references. You're correct of course that operator+= doesn't update the instance referred to by the left-hand side, it makes the LHS refer to a new instance. Think VB6 Let/Set.
Ben Voigt
@Ben: The values held by both fromList and the list *are* references. I'll clarify that bit though. Note that I didn't say they hold *objects*. I still maintain that just because two variables have the same reference doesn't mean they're connected... in particular, you can't get from `fromList` to `ms` or vice versa.
Jon Skeet
for some values of "connected". Apparently I'm thinking of "weak" connectivity (changes made via ms[0] may be detectable via fromList and vice-versa) and you're using "strong" connectivity (the address of ms can be determined from fromList and vice-versa).
Ben Voigt
A: 

Try to avoid implementing operators. Most of the time it doesn't worth the trouble and make code more confusing. Implement simple add/substract methods instead. Also, if you end up adding others values inside your object it will be easier to extend. By using operator, you use semantic related to the whole object to act on a part of it. Operators usually make more sense when used with struct. Structs are what you are supposed to use when implementing types that should behave like value types.

Simon T.
If your object can be added/subtracted, e.g. if it represents something that behaves like a number, then it makes perfect sense to overload operators. Furthermore, if you have something that behaves like an indexed container, it makes perfect sense to provide an indexer implementation, effectively overloading the `[]` operator. And this applies to classes just as much as (if not more than) to structs.
Daniel Earwicker
+3  A: 

You are mixing mutable behaviour with immutable behaviour, I think that's where it gets confusing.

The object is mutable (i.e. you can change it's properties), and it behaves as you would expect when you assign a value to it's property.

When you are using the + operator it behaves as an immutable value instead. Say that you had a list of integers, and read an integer into a variable. You wouldn't expext the integer in the list to change if you changed the value of the variable:

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;

When you have this code:

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

The compiler uses the + operator like this:

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

The expression fromlist + addie returns a reference to a new instance of the class, and that reference is assigned to the variable. That will naturally not change the object that the variable was referencing before.

Guffa
A: 

The relationship between compound assignment and simple operators is totally different in C# than in C++.

In C#, compound assignment operators call the simple operator which places the result in a new instance, then changes the original variable to refer to the new instance. The original referent isn't affected.

In C++, compound assignment operators mutate the referent. In most cases the simple arithmetic operator starts by cloning the LHS into a temporary, then invokes the compound assignment operator, mutating the temporary.

So the simple operators work the same in both, yielding a new instance, but the compound assignment operators always create a new instance in C# and not in C++.

Ben Voigt
It's not just *compound* assignment that's different. Simple assignment differs a lot too because C++ objects have copy semantics (like C# structs) while C# prefers reference semantics.
dan04
@dan: I meant to say "simple arithmetic" operators, not "simple assignment". Now fixed. Your statement about simple assignment is true only for reference types, which you hinted at. Of course reference types are a lot like C++ pointers (but automatically dereferenced except when assigning).
Ben Voigt
A: 

the real answer is that his + operator is not operating on the lhs object, it makes a copy and increments the copy. If his + operator actually changed the object then it would change the value in the list.

after

 M fromlist = ms[0];

fromlist and ms[0] both point to the same object. NOw

 fromlist += addie;

actually does

 fromlist = fromlist.op+(addie);

op+ returns a new M with val = fromlist.val + addie.val

so now fromlist and ms[0] point to different things with different values

pm100
A: 

Here's a different angle on this that I haven't seen in the other answers.

In C++ the way you make an array-like class is by overloading [] (known as the subscripting operator):

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

But that definition only lets you read items. To support writing, you need to return what we C++ fanboys will soon have to start calling an lvalue reference:

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

That means (at least in this context) that it can appear on the left side of an assignment expression:

list[3] = "hi";

But it raises all kinds of scary lifetime issues. If you can get a reference like that, you can bind it to a name:

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

Now oops refers to something in the internals of list. This is just not a very safe situation because some operations on list will cause oops to become a ticking time-bomb, and you need to have a detailed spec of how list works to avoid this.

C# works differently. A designer of an array-like class gets to define two separate definitions for the [] operator, as part of an indexer (the C# name for an implementation of the subscript operator):

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

This neatly bypasses the need for lvalue references. It also means that your simple example that works actually involves some nifty compiler footwork:

ms[0] += addie;

As many other answers have said, this is implemented using the ordinary + operator, so it expands to:

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

But those two occurrences of ms[0] are in fact calls to totally different methods. It's a bit like:

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

So that's why that example works. It replaces the object stored in the list. That's the step that is missing in your not-working example.

Daniel Earwicker