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
Table of Contents
18. Can you add type constructor to the interface?
In C# no. In IL — yes. See the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
// 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
.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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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:
1 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
// 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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:
1 2 3 4 |
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:
1 2 3 4 5 6 7 8 9 10 |
using System; public class Program { public static void Main() { object[] objects = new string[10]; objects[1] = 5; } } |
Output:
1 2 3 4 5 6 |
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:
1 2 3 4 5 6 7 8 9 10 |
using System; public class Program { public static void Main() { Action< string> derived = (string s) => Console.WriteLine(s); Action< object> @base = derived; } } |
Output:
1 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:
1 |
IL_0004: call void Foo::Extend(int32) |
In C# you could write it as:
1 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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:
1 2 3 4 5 6 7 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
// 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.