Reputation: 967
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";
}
Upvotes: 3
Views: 161
Reputation: 81307
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.
Upvotes: 1
Reputation: 391664
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.
Upvotes: 5
Reputation: 62265
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.
Upvotes: -1