3

I was looking at this question, and aside from a rather odd way to enumerate something, the op was having trouble because the enumerator is a struct. I understand that returning or passing a struct around uses a copy because it is a value type:

public MyStruct GetThingButActuallyJustCopyOfIt() 
{ 
    return this.myStructField; 
}

or

public void PretendToDoSomething(MyStruct thingy) 
{ 
    thingy.desc = "this doesn't work as expected"; 
}

So my question is if MyStruct implements IMyInterface (such as IEnumerable), will these types of methods work as expected?

public struct MyStruct : IMyInterface { ... }

//will caller be able to modify the property of the returned IMyInterface?
public IMyInterface ActuallyStruct() { return (IMyInterface)this.myStruct; }

//will the interface you pass in get its value changed?
public void SetInterfaceProp(IMyInterface thingy)
{
    thingy.desc = "the implementing type is a struct";
}
Community
  • 1
  • 1
mao47
  • 967
  • 10
  • 25

3 Answers3

5

Yes, that code will work, but it needs explanation, because there is a whole world of code that will not work, and you're likely to trip into that unless you know this.

Before I forget: Mutable structs are evil. OK, with that out of the way, let's move on.

Let's take a simple example, you can use LINQPad to verify this code:

void Main()
{
    var s = new MyStruct();
    Test(s);
    Debug.WriteLine(s.Description);
}

public void Test(IMyInterface i)
{
    i.Description = "Test";
}

public interface IMyInterface
{
    string Description { get; set; }
}

public struct MyStruct : IMyInterface
{
    public string Description { get; set; }
}

When executing this, what will be printed?

null

OK, so why?

Well, the problem is this line:

Test(s);

This will in fact box that struct and pass the boxed copy to the method. You're successfully modifying that boxed copy, but not the original s variable, which was never assigned anything, and is thus still null.

OK, so if we change just one line in the first piece of code:

IMyInterface s = new MyStruct();

Does this change the outcome?

Yes, because now you're boxing that struct here, and always use the boxed copy. In this context it behaves like an object, you're modifying the boxed copy and writing out the contents of the boxed copy.

The problem thus crops up whenever you box or unbox that struct, then you get copies that live separate lives.

Conclusion: Mutable structs are evil.

I see two answers about using ref here now, and this is barking up the wrong tree. Using ref means you've solved the problem before you added ref.

Here's an example.

If we change the Test method above to take a ref parameter:

public void Test(ref IMyInterface i)

Would this change anything?

No, because this code is now invalid:

var s = new MyStruct();
Test(ref s);

You'll get this:

The best overloaded method match for 'UserQuery.Test(ref UserQuery.IMyInterface)' has some invalid arguments

Argument 1: cannot convert from 'ref UserQuery.MyStruct' to 'ref UserQuery.IMyInterface'

And so you change the code to this:

IMyInterface s = new MyStruct();
Test(ref s);

But now you're back to my example, just having added ref, which I showed is not necessary for the change to propagate back.

So using ref is orthogonal, it solves different problems, but not this one.

OK, more comments regarding ref.

Yes, of course passing a struct around using ref will indeed make the changes flow throughout the program.

That is not what this question was about. The question posted some code, asked if it would work, and it would. In this particular variant of code it would work. But it's so easy to trip up. And pay particular note that the question was regarding structs and interfaces. If you leave interfaces out of it, and pass the struct around using ref, then what do you have? A different question.

Adding ref does not change this question, nor the answer.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Note that when I say "the problem is this line", the problem isn't really this line. The problem is really making the struct mutable. This is just one way to provoke all the problems you get with mutable structs. – Lasse V. Karlsen Jul 17 '13 at 14:03
  • Thanks, I guessed it would probably do something like boxing to act like an object, but wouldn't have spotted ways it could break. I wouldn't use a struct in this situation but was curious of the implications. – mao47 Jul 17 '13 at 14:11
  • The `ref` does change behavior (correctly) if used with a parameter of an interface-constrained generic type. For example, `bool AdvanceStringEnumerator(ref T it) where T:IEnumerator {return it.MoveNext();}` will advance a `List.Enumerator` to the next item even though that's a value type. I wish there was a way to define an interface in such a way that it could *only* be used as a constraint, since there are times when it's useful to have value types implement interfaces which only make sense with value types (and not with boxed equivalents). – supercat Jul 17 '13 at 18:48
  • @supercat: I think the problem is that while that make get the code to work as expected/desired, it isn't obviously necessary; my original code *appears* as it should work as expected. There is no warning or error, but the code doesn't do what is obviously intended. Luckily it seems rare that you would ever *need* to do something like this. I guess that is why they say immutable structs are evil. – mao47 Jul 17 '13 at 19:07
  • @mao47: I consider it unfortunate that some people are so hung up on the idea that "mutable structs are evil because when used improperly compilers will silently generate bogus code" that they aren't interested in providing a means by which compilers could generate warnings in such cases. While it's useful to have a standard means of defining types, and for capturing the state of a value type in a heap object, that doesn't mean one should expect every type to behave as an `Object`, any more than one should expect every fastener to behave like a nail. – supercat Jul 17 '13 at 19:21
  • @supercat: It looks like being able to change the boxing behavior of an interface as you said would be useful. Hmm, might another option be if the boxing operation wasn't automatic and was developer-specified, making it explicit that, "hey, this struct isn't supposed to act like an object, I hope you know what you're doing"? – mao47 Jul 17 '13 at 19:44
  • @mao47: I dislike implicit auto-boxing, though there are some places where it would be helpful for parameters to expressly allow it (e.g. arguments to `String.Format(...)`. It should conceptually be possible for `ValueType` to offer virtual boxing and unboxing methods in such fashion as to be compatible with existing code, though unless the code loader overrides the unboxing method for any instance which hasn't overridden it, each invocation of the latter method would require boxing a default instance of the type, copying data from the supplied object instance into that and then unboxing it. – supercat Jul 17 '13 at 19:57
1

Within the CLR, every value-type definition actually defines two kinds of things: a structure type, and a heap object type. A widening conversion exists from the structure type to the boxed object type, and a narrowing conversion exists from Object to the structure type. The structure type will behave with value semantics, and the heap object type will behave with mutable reference semantics. Note that the heap object types associated with all non-trivial structure types [i.e. those with any non-default states] are always mutable, and nothing in the structure definition can cause them to be otherwise.

Note that value types may be constrained, cast, or coerced to interface types, and cast or coerced to reference types. Consider:

void DoSomethingWithDisposable<T,U>(ref T p1, 
     List<int>.Enumerator p2) where T:IDisposable
{
  IDisposable v1a = p1; // Coerced
  Object v1b = p1; // Coerced
  IDisposable v2a = (IDisposable)p2; // Cast
  Object v2b = (Object)p2; // Cast
  p1.Dispose(); // Constrained call
}
void blah( List<string>.Enumerator p1, List<int>.Enumerator p2) // These are value types
{
  DoSomethingWithDisposable(p1,p2); // Constrains p1 to IDisposable
}

Constraining a generic type to an interface type does not affect its behavior as a value type. Casting or coercing an a value type to an interface or reference type, however, will create a new instance of the heap object type and return a reference to that. That reference will then behave with reference-type semantics.

The behavior of value types with generic constraints can at times be very useful, and such usefulness can apply even when using mutating interfaces, but unfortunately there's no way to tell the compiler that a value type must remain as a value type, and that the compiler should warn if it would find itself converting it to something else. Consider the following three methods:

bool AdvanceIntEnumerator1(IEnumerator<int> it)
  { return it.MoveNext(); }

bool AdvanceIntEnumerator2(ref T it) where T:IEnumerator<int>
  { return it.MoveNext(); }

bool AdvanceIntEnumeratorTwice<T>(ref T it) where T:IEnumerator<int>
  { return it.MoveNext() && AdvanceIntEnumerator1(it); }

If one passes to the first piece of code a variable of type List<int>.Enumerator, the system will copy its state to a new heap object, call MoveNext on that object, and abandon it. If one passes instead a variable of type IEnumerator<int> which holds a reference to a heap object of type List<int>.Enumerator, it will call MoveNext on that instance, which the calling code will still retain.

If one passes to the second piece of code a variable of type List<int>.Enumerator, the system will call MoveNext on that variable, thus changing its state. If one passes a variable of type IEnumerable<T>, the system will call MoveNext on the object referred to by that variable; the variable won't be modified (it will still point to the same instance), but the instance to which it points will be.

Passing to the third piece of code a variable of type List<int>.Enumerator will cause MoveNext to be called on that variable, thus changing its state. If that returns true, the system will copy the already-modified variable to a new heap object and call MoveNext on that. The object will then be abandoned, so the variable will only be advanced once, but the return value will indicate whether a second MoveNext would have succeeded. Passing the third piece of code a variable of type IEnumerator<T> which holds a reference to a List<T>.Enumerator, however, will cause that instance to be advanced twice.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • +1 Wow, what a detailed answer. Yours and the accepted both deserve many many votes. – mao47 Jul 17 '13 at 16:04
  • 1
    @mao47: It irks me that the language teams don't want to add a means (e.g. define and honor an attribute) via which structure types could specify which operations may modify the underlying struct; they claim that "mutable structs are evil", and thus don't want to add features that would enhance their usefulness, but the only reason they're "evil" in the first place is that the compilers provide no means of indicating that certain constructs should generate warnings instead of silently producing bogus code. – supercat Jul 17 '13 at 16:16
-1

No, interface is a contract, to make it work properly you need to use ref keyword.

public void SetInterfaceProp(ref IMyInterface thingy)
{
    thingy.desc = "the implementing type is a struct";
}

What matters here is a real type that stays inside that interface wrap.

To be more clear:

even if code with method SetInterfaceProp defined like

public void SetInterfaceProp(IMyInterface thingy)
{
    thingy.desc = "the implementing type is a struct";
}

will work:

IMyInterface inter= default(MyStruct); 
SetInterfaceProp(inter);

this one will not :

MyStruct inter = default(MyStruct); 
SetInterfaceProp(inter);

You can not gurantee that the caller of your method will always use IMyInterface, so to guarantee expected behavior, in this case, you can define ref keyword, that will guarantee that in both cases method would run as expected.

Tigran
  • 61,654
  • 8
  • 86
  • 123
  • The reason is that `ref` is completely orthogonal to this. The only reason `ref` works is that you've already boxed the struct into an interface reference. `ref` does not change this, you can take it out. Note that using `ref` if you're passing in a struct type that just happens to implement the interface won't compile, and that's why I know that you've already done the boxing. `ref` solves different problems, but not this one. – Lasse V. Karlsen Jul 17 '13 at 14:04
  • @LasseV.Karlsen: I can use *ref* without interface definition either, and it *will* work as expected (will change the property of instance inside method). I can use it *with* interface definition and it, again, will work as expected. – Tigran Jul 17 '13 at 14:07
  • @LasseV.Karlsen: my point here was, to provide a signature where either I pass a struct or an interface inside a method, method would give me expected result. – Tigran Jul 17 '13 at 14:09
  • Try passing a struct to a method that takes a ref interface, ie. this: `MyStruct s = new MyStruct(); SetInterfaceProp(ref s);` This won't compile. The only way you can get it to compile is by declaring the variable to be of type `IMyInterface`, but then you don't need the `ref`. In other words, your last piece of code in your answer will not work simply by adding `ref`, you'll get a compiler error. – Lasse V. Karlsen Jul 17 '13 at 14:10
  • @LasseV.Karlsen: ok but see my example, in the answer. Even using signature you suggest, there is easy way to *compile* and get *wrong* result. Introducing *ref* gives you possibility to use IMyInterface instance like a parameter *or* MyStruct instance like a parameter, and in both cases method works. I mean, this is a design issue. It depends *how* and *where* this function used. – Tigran Jul 17 '13 at 14:15
  • 1
    Of course it's easy to get the wrong value, that's the whole problem with mutable structs. The question, **however** was whether the particular code would work. And it will, but having noticed that (the OP I mean), he could conclude that using this type of code is OK, but it's not. Again, `ref` does nothing here. – Lasse V. Karlsen Jul 17 '13 at 14:17
  • @LasseV.Karlsen: The proper way to get it to compile is to use a `ref` parameter of an interface-constrained generic type: `void SetInterfaceProp(ref T it) where T:IMyInterface {...}`. That will have value semantics when used with types that have value semantics, and reference semantics when used with reference types. One could change the code to `where T:struct,IMyInterface` if one wanted to ensure that it would only compile when used with types that at least superficially have value semantics. – supercat Jul 17 '13 at 18:54