41

When we invoke a method on a object, then the reference of the object is passed implicitly to the method.

So my question is what happens when a method is invoked on a struct ? Is it similar to classes in this aspect?

svick
  • 236,525
  • 50
  • 385
  • 514
Searock
  • 6,278
  • 11
  • 62
  • 98

3 Answers3

38

According to the CIL spec the this pointer is passed by reference / as managed pointer. As Binary Worrier assumed this is a CIL feature.

Instance and virtual methods of classes shall be coded to expect a reference to an instance of the class as the this pointer. By contrast, instance and virtual methods of value types shall be coded to expect a managed pointer (see Partition I) to an unboxed instance of the value type. The CLI shall convert a boxed value type into a managed pointer to the unboxed value type when a boxed value type is passed as the this pointer to a virtual method whose implementation is provided by the unboxed value type.

So from a high-level perspective a call to an instance method of a reference type (class) looks like this:

MyClass myClass = MyClass_Constructor();

MyClass_MyInstanceMethod(myClass, myFirstParameter);
//                       ^
//                       The "this" argument

And a call to an instance method of a value type (struct) like this:

MyStruct myStruct = MyStruct_Constructor();

MyStruct_MyInstanceMethod(ref myStruct, myFirstParameter);
//                        ^
//                        The "this" argument
svick
  • 236,525
  • 50
  • 385
  • 514
Florian Greinacher
  • 14,478
  • 1
  • 35
  • 53
31

Florian is right; a few more details while we're on the subject:

When we invoke a method on a object, then the reference of the object is passed implicitly to the method.

Correct. One way to think about this is that a method call like:

class C
{
    int y;
    public void M(int x) 
    { 
        Console.WriteLine(x + y); 
    }
}
...
C c = new C();
c.M(10);

is actually the same as

class C
{
    int y;
    public static void M(C _this, int x) 
    { 
        Console.WriteLine(x + _this.y); 
    }
}
...
C c = new C();
C.M(c, 10);

That is, every instance method has a hidden "this" parameter, and the method is "really" static.

So my question is what happens when a method is invoked on a struct ? Is it similar to classes in this aspect?

Yes. Instead of a reference to an instance, what is passed is an alias to the variable that contains the struct. That's how struct methods can mutate the struct. (Of course, mutating a struct is a bad practice, but sometimes necessary.)

struct S
{
    int y;
    public void M(int x) 
    { 
        Console.WriteLine(x + y); 
    }
}
...
S s = new S();
s.M(10);

is logically the same as

struct S
{
    int y;
    public static void M(ref S _this, int x) 
    { 
        Console.WriteLine(x + _this.y); 
    }
}
...
S s = new S();
S.M(ref s, 10);

An interesting question then arises: what if the "receiver" is not a variable? (*) You can only take a ref to a variable. Suppose you have:

GetAnS().M(10); 

??? What happens then?

We make a variable for you. That becomes

S temporary = GetAnS();
temporary.M(10);

and now the receiver is a variable, so we can make the "hidden this parameter" an alias to it.

(*) There are other interesting cases here as well, like what if the struct is mutable but the variable is readonly, and so on.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 1
    Eric Lippert +1 Thanks for the explanation. – Searock May 06 '11 at 09:06
  • 1
    Does that mean that if I have a custom value type that's smaller than a pointer on my architecture, it's more efficient to use an extension method than an instance method on the struct, because there's fewer bytes to copy to the method's stack frame? – Benjamin Hodgson Oct 08 '17 at 13:47
  • 2
    I know how to find out. Try it both ways and see which is more efficient by your empirical measure of efficiency. – Eric Lippert Oct 08 '17 at 13:56
  • Good explanation, But I will be noted additionally: After the call the "S.M(ref s, 10);" - S can change its content (mutable). After the call the "temporary.M(10);" - all changes to temporary variable will be lost, because after the call the temporary variable will be disappeared – Denis Sivtsov Nov 18 '20 at 20:57
2

I wrote a series of blog posts on exactly this subject a while back. In particular, http://www.simple-talk.com/community/blogs/simonc/archive/2010/11/02/95489.aspx

In IL, you can store a managed pointer on the stack, which is like an object reference, except it can point at things other than object references. There are specific instructions to get a managed pointer to something, such as ldloca, which gets a pointer to a local variable, or ldelema, which gets a pointer to a particular element in an array.

These are safe pointers, as the garbage collector knows about them, and so changes them if the object you're pointing to is moved by the GC.

Method calls on structs require a managed pointer to the struct (which can be anywhere in memory - on the stack, in an argument list, in another object on the heap) the method is to be executed on as the this pointer. Method calls on reference types require an object reference.

This causes some issues when calling methods on generic types, which can be either value or reference types. I have a look at that in later blog posts.

One of the side effects of being able to use managed pointers to value types is that it is possible in IL to call a method on a boxed value type without having to unbox it:

// you have a struct on the stack
box MyStructType    // this copies the value on the stack to an object on the heap
unbox MyStructType  // this returns a managed pointer to the value type instance in its boxed form. It doesn't copy the value itself back to the stack (despite the name)
call instance void MyStructType::MyMethod()
thecoop
  • 45,220
  • 19
  • 132
  • 189