28

.NET Internals Cookbook Part 4 Type members

 5 years ago
source link: https://www.tuicool.com/articles/hit/uiUJ7nr
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

This is the fourth part of the .NET Internals Cookbook series. For your convenience you can find other parts in the table of contents in Part 0 – Table of contents

18. Can you add type constructor to the interface?

In C# no. In IL — yes. See the code below:

// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly '51e89a1e-6120-4a32-a6f0-bbbe95846cbc'
{
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module '51e89a1e-6120-4a32-a6f0-bbbe95846cbc.dll'
// MVID: {DC9AB443-034C-4EFA-9D7C-D8DE9E14DA85}
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x01030000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
	.entrypoint
    .maxstack  1
    .locals init ([0] class IFoo foo)
    .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
    .line 6,6 : 2,3 ''
    IL_0000:  nop
    .line 7,7 : 3,24 ''
    IL_0001:  newobj     instance void Foo::.ctor()
    IL_0006:  stloc.0
    .line 8,8 : 3,13 ''
    IL_0007:  ldloc.0
    IL_0008:  callvirt   instance void IFoo::Bar()
    IL_000d:  nop
    .line 9,9 : 2,3 ''
    IL_000e:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class Program

.class interface public abstract auto ansi IFoo
{
  .method private hidebysig specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // 
    .maxstack  8
    .line 19,19 : 14,15 ''
    IL_0000:  nop
    .line 20,20 : 3,45 ''
    IL_0001:  ldstr      "IFoo type constructor"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    .line 21,21 : 2,3 ''
    IL_000c:  ret
  } // end of method IFoo::.cctor
  
  .method public hidebysig newslot abstract virtual 
          instance void  Bar() cil managed
  {
  } // end of method IFoo::Bar

} // end of class IFoo

.class public auto ansi Foo
       extends [mscorlib]System.Object
       implements IFoo
{
  .method private hidebysig specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // 
    .maxstack  8
    .line 19,19 : 14,15 ''
    IL_0000:  nop
    .line 20,20 : 3,45 ''
    IL_0001:  ldstr      "Foo type constructor"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    .line 21,21 : 2,3 ''
    IL_000c:  ret
  } // end of method Foo::.cctor

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 
    .maxstack  8
    .line 23,23 : 2,14 ''
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    .line 23,23 : 14,15 ''
    IL_0007:  nop
    .line 24,24 : 3,49 ''
    IL_0008:  ldstr      "Foo instance constructor"
    IL_000d:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0012:  nop
    .line 25,25 : 2,3 ''
    IL_0013:  ret
  } // end of method Foo::.ctor

  .method public hidebysig newslot virtual final 
          instance void  Bar() cil managed
  {
    // 
    .maxstack  8
    .line 27,27 : 19,20 ''
    IL_0000:  nop
    .line 28,28 : 3,28 ''
    IL_0001:  ldstr      "Bar"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    .line 29,29 : 2,3 ''
    IL_000c:  ret
  } // end of method Foo::Bar

} // end of class Foo


// =============================================================

//

If you are not familiar with IL then just follow the comments and this is the part you should be looking for:

.class interface public abstract auto ansi IFoo
{
  .method private hidebysig specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // 
    .maxstack  8
    .line 19,19 : 14,15 ''
    IL_0000:  nop
    .line 20,20 : 3,45 ''
    IL_0001:  ldstr      "IFoo type constructor"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    .line 21,21 : 2,3 ''
    IL_000c:  ret
  } // end of method IFoo::.cctor
  
  .method public hidebysig newslot abstract virtual 
          instance void  Bar() cil managed
  {
  } // end of method IFoo::Bar

} // end of class IFoo

We have an interface here and a method named cctor . There are special names in IL, constructors are named ctor whereas type constructors (or type initializers) are considered a class constructors hence the name cctor .

However, it is not very useful as the constructor is not called. You can specify type constructor for structs as well, but it may be ignored in some situations. For instance, “The creation of default values (§18.3.4) of struct types does not trigger the static constructor” (from the C# standard).

19. How to change the name of the indexer?

Using this code :

using System;

public class Program
{
	public static void Main()
	{
		Foo foo = new Foo();
		Console.WriteLine(foo[1]);
	}
}

class Foo
{
	[System.Runtime.CompilerServices.IndexerName("TheItem")] 
	public int this[int i]
	{
		get
		{
			return i;
		}

		set
		{
		}
	}
}

When you decompile it, you can see that it calls:

IL_0009:  callvirt   instance int32 Foo::get_TheItem(int32)

Why would you do that? If you use language without direct support for indexers (you do remember there are over 20 languages running on CLR , don’t you?), you need to call them by name.

20. Can you have static fields and methods in the interface?

Yes, you can. See this code:

// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly 'cced1cd5-fc6a-40bb-934a-8a8958e42648'
{
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module 'cced1cd5-fc6a-40bb-934a-8a8958e42648.dll'
// MVID: {65C0D9A7-200B-4DE7-9B06-E490729C037F}
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x01260000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
	.entrypoint
    .maxstack  8
    .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
    .line 6,6 : 2,3 ''
    IL_0000:  nop
    .line 7,7 : 3,13 ''
    IL_0001:  call       void IFoo::Bar()
    IL_0006:  nop
    .line 8,8 : 2,3 ''
    IL_0007:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class Program

.class interface public abstract auto ansi IFoo
{
  .method public hidebysig static void  Bar() cil managed
  {
    // 
    .maxstack  8
    .line 17,17 : 26,27 ''
    IL_0000:  nop
    .line 18,18 : 3,28 ''
    IL_0001:  ldstr      "Bar"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    .line 19,19 : 2,3 ''
    IL_000c:  ret
  } // end of method IFoo::Bar
} // end of class IFoo

C# doesn’t support it, but in IL you can define static field, method or type constructor.

21. How many objects are created in new Foo {Bar = 1} ?

One. The property is then assigned by calling the setter.

See this code

using System;

public class Program
{
	public static void Main()
	{
		Foo foo = new Foo { Bar = 5};
	}
}

class Foo
{
	public int Bar {get; set;}
}

When you decompile it you get this:

IL_0001:  newobj     instance void Foo::.ctor()
IL_0006:  dup
IL_0007:  ldc.i4.5
IL_0008:  callvirt   instance void Foo::set_Bar(int32)

Clearly, there is one object and a setter call.

22. What is covariance? Contravariance? How does it work with arrays and with generics?

This allows you to return or accept subtype when base type is expected. This short explanation actually has a lot of intricate details which you may have just ignored.

First, type and class are not the same. Type specifies requirements imposed on an object whereas class is a concrete implementation of those. In C# we have classes only but in other languages you can have types without classes or object-oriented programming without classes at all. Oddly enough, JS is not considered an object-oriented language even though it is based on objects everywhere whereas C# is considered OOP but it is based on classes (maybe we should call it “class-oriented language”? Let the flamewar begin !).

This also means that we can have a subtype and not a subclass at the same time or a subclass which is not a subtype! Technically we can consider nominal subtyping, structural subtyping, subtyping without subclassing and many other combinations. I highly recommend reading A Theory of Objects which explains it in depth. Also, playing with JS or Scala can be beneficial as those languages have very interesting type systems.

Let’s get back to C#. Imagine there is a class Base and a class Derived : Base . We can consider IEnumerable< Derived> being an instance of IEnumerable< Base> because Derived is a subtype of Base . Thanks to that we can assign IEnumerable< Derived> to IEnumerable< Base> and it works. Typically we can use it when returning values from methods — we often say that “return value” is in a “covariant position” which is true for most of the times.

The same goes for input parameters: we can create a delegate of type Action< Base> and assign to it an instance of Action< Derived> and it does work. We say that “input parameter” is in a “contravariant position” (once again, true for most of the times).

Covariance is supported in arrays, delegates (also called “method group variance”) and generic type parameters in interfaces and delegates. It is marked using out keyword in C#. Contravariance is supported in delegates and generic type parameters in interfaces and delegates. It is marked with in keyword.

Covariance in arrays is dangerous. It is supported because Java had it before C# emerged and then C# just copied it. But it can easily break your application :

using System;

public class Program
{
	public static void Main()
	{
		object[] objects = new string[10];
		objects[1] = 5;
	}
}

Output:

Run-time exception (line -1): Attempted to access an element as a type incompatible with the array.

Stack Trace:

[System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.]
   at Program.Main()

You use an array of strings as an array of objects. This is fine unless you assign a non-string to its content. If you do — you get an exception in runtime. Fortunately, most of the times compiler can find those subtle bugs when we use generic types.

When it comes to input parameters, compiler can figure it out with contravariant positions :

using System;

public class Program
{
	public static void Main()
	{
		Action< string> derived = (string s) => Console.WriteLine(s);
		Action< object> @base = derived;
	}
}

Output:

Compilation error (line 8, col 26): Cannot implicitly convert type 'System.Action< string>' to 'System.Action< object>'. An explicit conversion exists (are you missing a cast?)

We have a lambda accepting strings and printing them out and then we want to use it as a lambda accepting objects. This clearly can’t work as we would be allowed to pass object as a string parameter. So generally input position is contravariant whereas output position is covariant.

But things get really tricky when you mix covariant and contravariant positions :

using System;

public class Program
{
	public static void Main()
	{
		Action< Base> baseAction = (Base b) => Console.WriteLine("Base: " + b);
		Action< Derived> derivedAction = baseAction;
		// Action< Base> baseAction2 = derivedAction; // Doesn't work because you could then call derivedAction(new Base()) which is wrong
		
		Action< Action< Derived>> derivedHigherOrderFunction = (Action< Derived> action) => {
			action(new Derived());
		};
		
		Action< Action< Base>> baseHigherOrderFunction = derivedHigherOrderFunction;
		
		baseHigherOrderFunction(baseAction);
	}
}

public class Base{}

public class Derived : Base{}

We see that with higher order function we can reverse the relationship of subtypes!

See also this great series by Eric Lippert explaining how things get very tricky with higher order functions and this great post by Tomasz Sitarek (polish language only) to see the difference between a type and a class, and this Alexandra Rusina’s post showing multiple examples.

23. Can you use extension methods with dynamic ?

No. See this code :

using System;

public class Program
{
	public static void Main()
	{
		int x = 5;
		x.Extend();
		dynamic y = x;
		y.Extend();
	}
}

static class Foo
{
	public static void Extend(this int x){
		Console.WriteLine(x);
	}
}

This is because extension methods are compiled as static ones. This code in fact does that:

IL_0004:  call       void Foo::Extend(int32)

In C# you could write it as:

Foo.Extend(x);

However, DLR looks for methods of the object so you cannot use extensions with dynamic code.

As a side note, this actually explains why creating traits using just extension methods is not very useful (unless we implement polymorphism manually . That’s because when you call a method the call is bound statically so you have no dynamic dispatch which is the whole point of trait mechanism.

Yet another side note, Java introduced default methods in interfaces which can be used to build very basic traits. They do support polymorphism (so they are dynamically bound) but you cannot call base method (super in Java nomenclature) out of the box, you need to specify the superclass (base implementation) explicitly. Hopefully C# gets this right (or we can always implement polymorphism manually ).

24. What is the difference between Equals and == ?

Equals is bound in runtime, == is bound in compile time. See this :

using System;

public class Program
{
	public static void Main()
	{
		Foo one = new Foo();
		Foo two = new Foo();
		Console.WriteLine("Foo one AND Foo two");
		bool a = one.Equals(two);
		bool b = one == two;
		Console.WriteLine("Foo one AND object three (which is still Foo)");
		object three = two;
		bool c = one.Equals(three);
		bool d = one == three;
		Console.WriteLine("object three (which is still Foo) AND Foo two");
		bool e = three.Equals(one);
		bool f = three == one;
	}
}

class Foo
{
	public override bool Equals(object that){
		Console.WriteLine("Custom Equals");
		return base.Equals(that);
	}
	
	public static bool operator == (Foo lhs, Foo rhs) {
        Console.WriteLine("Custom ==");
		return object.ReferenceEquals(lhs, rhs);
    }
	
	public static bool operator != (Foo lhs, Foo rhs) {
        Console.WriteLine("Custom !=");
		return !object.ReferenceEquals(lhs, rhs);
    }
}

Output:

Foo one AND Foo two
Custom Equals
Custom ==
Foo one AND object three (which is still Foo)
Custom Equals
object three (which is still Foo) AND Foo two
Custom Equals

You can see that Equals uses actual object types whereas == is bound statically. If you decompile the code you get:

IL_000d:  ldstr      "Foo one AND Foo two"
IL_0012:  call       void [mscorlib]System.Console::WriteLine(string)
IL_0017:  nop
.line 10,10 : 3,28 ''
IL_0018:  ldloc.0
IL_0019:  ldloc.1
IL_001a:  callvirt   instance bool [mscorlib]System.Object::Equals(object)
IL_001f:  stloc.2
.line 11,11 : 3,23 ''
IL_0020:  ldloc.0
IL_0021:  ldloc.1
IL_0022:  call       bool Foo::op_Equality(class Foo,
										   class Foo)
IL_0027:  stloc.3
.line 12,12 : 3,70 ''
IL_0028:  ldstr      "Foo one AND object three (which is still Foo)"
IL_002d:  call       void [mscorlib]System.Console::WriteLine(string)
IL_0032:  nop
.line 13,13 : 3,22 ''
IL_0033:  ldloc.1
IL_0034:  stloc.s    three
.line 14,14 : 3,30 ''
IL_0036:  ldloc.0
IL_0037:  ldloc.s    three
IL_0039:  callvirt   instance bool [mscorlib]System.Object::Equals(object)
IL_003e:  stloc.s    c
.line 15,15 : 3,25 ''
IL_0040:  ldloc.0
IL_0041:  ldloc.s    three
IL_0043:  ceq
IL_0045:  stloc.s    d
.line 16,16 : 3,70 ''
IL_0047:  ldstr      "object three (which is still Foo) AND Foo two"
IL_004c:  call       void [mscorlib]System.Console::WriteLine(string)
IL_0051:  nop
.line 17,17 : 3,30 ''
IL_0052:  ldloc.s    three
IL_0054:  ldloc.0
IL_0055:  callvirt   instance bool [mscorlib]System.Object::Equals(object)
IL_005a:  stloc.s    e
.line 18,18 : 3,25 ''
IL_005c:  ldloc.s    three
IL_005e:  ldloc.0
IL_005f:  ceq
IL_0061:  stloc.s    f

You can see that for non-overloaded == operator it uses ceq instruction.

25. How to write parameterless constructor for a struct?

Once again, you can’t do that in C# but you can in IL.

// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly '2b7df92a-f0d3-46a5-bbb9-7916c279dd6b'
{
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module '2b7df92a-f0d3-46a5-bbb9-7916c279dd6b.dll'
// MVID: {99CB3BB6-954C-4B95-8D52-2D78D1691810}
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x01C60000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
	.entrypoint
    .maxstack  1
    .locals init ([0] valuetype Foo one,
             [1] valuetype Foo two)
    .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
    .line 6,6 : 2,3 ''
    IL_0000:  nop
    .line 7,7 : 3,24 ''
    IL_0001:  ldloca.s   one
    IL_0004:  call       instance void Foo::.ctor()
    .line 8,8 : 3,26 ''
    IL_0009:  ldloca.s   two
    IL_000b:  initobj    Foo
    .line 9,9 : 2,3 ''
    IL_0011:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class Program

.class private sequential ansi sealed beforefieldinit Foo
       extends [mscorlib]System.ValueType
{
  .pack 0
  .size 1
  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 
    .maxstack  8
    .line 13,13 : 19,20 ''
    IL_0000:  nop
    .line 14,14 : 3,50 ''
    IL_0001:  ldstr      "Parameterless constructor"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    .line 15,15 : 2,3 ''
    IL_000c:  ret
  } // end of method Foo::.ctor

} // end of class Foo


// =============================================================

//

C# 6.0 was supposed to introduce a way of implementing parameterless constructors but it was removed because of a bug in Activator.CreateInstance

See also this John Skeet’s post .

26. What is the difference between new Struct() and default(Struct) ?

The former calls parameterless constructor, the latter creates new instance and with all fields zeroed out. Most likely this doesn’t make a difference for you.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK