简体   繁体   中英

Details on what happens when a struct implements an interface

I recently came across this Stackoverflow question: When to use struct?

In it, it had an answer that said something a bit profound:

In addition, realize that when a struct implements an interface - as Enumerator does - and is cast to that implemented type, the struct becomes a reference type and is moved to the heap. Internal to the Dictionary class, Enumerator is still a value type. However, as soon as a method calls GetEnumerator(), a reference-type IEnumerator is returned.

Exactly what does this mean?

If I had something like

struct Foo : IFoo 
{
  public int Foobar;
}

class Bar
{
  public IFoo Biz{get; set;} //assume this is Foo
}

...

var b=new Bar();
var f=b.Biz;
f.Foobar=123; //What would happen here
b.Biz.Foobar=567; //would this overwrite the above, or would it have no effect?
b.Biz=new Foo(); //and here!?

What exactly are the detailed semantics of a value-type structure being treated like a reference-type?

Every declaration of a structure type really declares two types within the Runtime: a value type, and a heap object type. From the point of view of external code, the heap object type will behave like a class with a fields and methods of the corresponding value type. From the point of view of internal code, the heap type will behave as though it has a field this of the corresponding value type.

Attempting to cast a value type to a reference type ( Object , ValueType , Enum , or any interface type) will generate a new instance of its corresponding heap object type, and return a reference to that new instance. The same thing will happen if one attempts to store a value type into a reference-type storage location, or pass it as a reference-type parameter. Once the value has been converted to a heap object, it will behave--from the point of view of external code--as a heap object.

The only situation in which a value type's implementation of an interface may be used without the value type first being converted to a heap object is when it's passed as a generic type parameter which has the interface type as a constraint. In that particular situation, interface members may be used on the value type instance without its having to be converted to a heap object first.

Read about boxing and unboxing (search the internet). For example MSDN: Boxing and Unboxing (C# Programming Guide) .

See also the SO thread Why do we need boxing and unboxing in C#? , and the threads linked to that thread.

Note: It is not so important if you "convert" to a base class of the value type, as in

object obj = new Foo(); // boxing

or "convert" to an implemented interface, as in

IFoo iFoo = new Foo(); // boxing

The only base classes a struct has, are System.ValueType and object (including dynamic ). The base classes of an enum type are System.Enum , System.ValueType , and object .

A struct can implement any number of interfaces (but it inherits no interfaces from its base classes). An enum type implements IComparable (non-generic version), IFormattable , and IConvertible because the base class System.Enum implements those three.

I'm replying your post about your experiment on 2013-03-04, though I might be a bit late :)

Keep this in mind: Every time you assign a struct value to a variable of an interface type (or return it as an interface type) it will be boxed. Think of it like a new object (the box) will be created on the heap, and the value of the struct will be copied there. That box will be kept until you have a reference on it, just like with any object.

With behavior 1, you have the Biz auto property of type IFoo, so when you set a value here, it will be boxed and the property will keep a reference to the box. Whenever you get the value of the property, the box will be returned. This way, it mostly works as if Foo would be a class, and you get what you expect: you set a value and you get it back.

Now, with behavior 2, you store a struct (field tmp), and your Biz property returns its value as an IFoo. That means every time get_Biz is called, a new box will be created and returned .

Look through the Main method: every time you see a b.Biz, that's a different object (box). That will explain the actual behavior.

Eg in line

    b.Biz.Foobar=567;

b.Biz returns a box on the heap, you set the Foobar in it to 576 and then, as you do not keep a reference to it, it is lost immediatly for your program.

In the next line you writeline b.Biz.Foobar, but this call to b.Biz will then again create a quite new box with Foobar having the default 0 value, that's what printed.

Next line, variable f earlier was also filled by a b.Biz call which created a new box, but you kept a reference for that (f) and set its Foobar to 123, so that's still what you have in that box for the rest of the method.

So, I decided to put this behavior to the test myself. I'll give the "results", but I can't explain why things happen this way. Hopefully someone with more knowledge about how this works can come along and enlighten me with a more thorough answer

Full test program:

using System;

namespace Test
{
    interface IFoo
    {
        int Foobar{get;set;}
    }
    struct Foo : IFoo 
    {
        public int Foobar{ get; set; }
    }

    class Bar
    {
        Foo tmp;
        //public IFoo Biz{get;set;}; //behavior #1
        public IFoo Biz{ get { return tmp; } set { tmp = (Foo) value; } } //behavior #2

        public Bar()
        {
            Biz=new Foo(){Foobar=0};
        }
    }


    class MainClass
    {
        public static void Main (string[] args)
        {
            var b=new Bar();
            var f=b.Biz;
            f.Foobar=123; 
            Console.WriteLine(f.Foobar); //123 in both
            b.Biz.Foobar=567; /
            Console.WriteLine(b.Biz.Foobar); //567 in behavior 1, 0 in 2
            Console.WriteLine(f.Foobar); //567 in behavior 1, 123 in 2
            b.Biz=new Foo();
            b.Biz.Foobar=5;
            Console.WriteLine(b.Biz.Foobar); //5 in behavior 1, 0 in 2
            Console.WriteLine(f.Foobar); //567 in behavior 1, 123 in 2
        }
    }
}

As you can see, by manually boxing/unboxing we get extremely different behavior. I don't completely understand either behavior though.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM