How does a generic constraint prevent boxing of a value type with an implicitly implemented interface?

前端 未结 5 1974
南笙
南笙 2020-12-08 12:12

My question is somewhat related to this one: Explicitly implemented interface and generic constraint.

My question, however, is how the compiler enables a ge

相关标签:
5条回答
  • 2020-12-08 12:16

    My question, however, is how the compiler enables a generic constraint to eliminate the need for boxing a value type that explicitly implements an interface.

    By "the compiler" it is not clear whether you mean the jitter or the C# compiler. The C# compiler does so by emitting the constrained prefix on the virtual call. See the documentation of the constrained prefix for details.

    What is going on with the behind-the-scenes CLR implementation that requires a value type to be boxed when accessing an explicitly implemented interface member

    Whether the method being invoked is an explicitly implemented interface member or not is not particularly relevant. A more general question would be why does any virtual call require the value type to be boxed?

    One traditionally thinks of a virtual call as being an indirect invocation of a method pointer in a virtual function table. That's not exactly how interface invocations work in the CLR, but it's a reasonable mental model for the purposes of this discussion.

    If that's how a virtual method is going to be invoked then where does the vtable come from? The value type doesn't have a vtable in it. The value type just has its value in its storage. Boxing creates a reference to an object that has a vtable set up to point to all the value type's virtual methods. (Again, I caution you that this is not exactly how interface invocations work, but it is a good way to think about it.)

    What happens with a generic constraint that removes this requirement?

    The jitter is going to be generating fresh code for each different value type argument construction of the generic method. If you're going to be generating fresh code for each different value type then you can tailor that code to that specific value type. Which means that you don't have to build a vtable and then look up what the contents of the vtable are! You know what the contents of the vtable are going to be, so just generate the code to invoke the method directly.

    0 讨论(0)
  • 2020-12-08 12:16

    The generic constraint provides only a compile time check that the correct type is being passed into the method. The end result is always that the compiler generates an appropriate method that accepts the runtime type:

    public struct Foo : IFoo { }
    
    public void DoSomething<TFoo>(TFoo foo) where TFoo : IFoo
    {
      // No boxing will occur here because the compiler has generated a
      // statically typed DoSomething(Foo foo) method.
    }
    

    In this sense, it bypasses the need for boxing of value types, because an explicit method instance is created that accepts that value type directly.

    Whereas when a value type is cast to an implemented interface, the instance is a reference type, which is located on the heap. Because we don't take advantage of generics in this sense, we are forcing a cast to an interface (and subsequent boxing) if the runtime type is a value type.

    public void DoSomething(IFoo foo)
    {
      // Boxing occurs here as Foo is cast to a reference type of IFoo.
    }
    

    Removal of the generic constraint only stops the compile time checking that your passing the correct type into the method.

    0 讨论(0)
  • 2020-12-08 12:26

    The ultimate goal is to get a pointer to the method table of the class so that the correct method can be called. That can't happen directly on a value type, it is just a blob of bytes. There are two ways to get there:

    • Opcodes.Box, implements the boxing conversion and turns the value type value into an object. The object has the method table pointer at offset 0.
    • Opcodes.Contrained, hands the jitter the method table pointer directly without the need for boxing. Enabled by the generic constraint.

    The latter is clearly more efficient.

    0 讨论(0)
  • 2020-12-08 12:34

    I think you need to use

    • reflector
    • ildasm / monodis

    to really get the answer you want

    You can of course look into the specs of the CLR (ECMA) and or the source of a C# compiler (mono)

    0 讨论(0)
  • 2020-12-08 12:38

    Boxing is necessary when a value-type object is passed to a routine that expects to receive a class-type object. A method declaration like string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String> actually declares a whole family of functions, each of which expects a different type T. If T happens to be a value type (e.g. List<String>.Enumerator), the Just-In-Time compiler will actually generate machine code exclusively to perform ReadAndAdvanceEnumerator<List<String>.Enumerator>(). BTW, note the use of ref; if T were a class type (interface types used in any context other than constraints count as class types) the use of ref would be an unnecessary impediment to efficiency. If, however, there's a possibility that T might be a this-mutating struct (e.g. List<string>.Enumerator), the use of ref will be necessary to ensure that this mutations performed by the struct during the execution of ReadAndAdvanceEnumerator will be performed upon the caller's copy.

    0 讨论(0)
提交回复
热议问题