This is the fifth 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
27. Can two methods differ in return type only?
Not in C#, but they can in IL. 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 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 |
// 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 '9ebd3665-50ff-4ecf-b716-a61e4b5c1f81' { .hash algorithm 0x00008004 .ver 0:0:0:0 } .module '9ebd3665-50ff-4ecf-b716-a61e4b5c1f81.dll' // MVID: {79C4F8CD-B0E4-4474-8A80-82F28C5D6F41} .imagebase 0x10000000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000001 // ILONLY // Image base: 0x01530000 // =============== 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] int32 x, [1] float64 y) .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,17 '' IL_0001: call int32 Program::Foo() IL_0006: stloc.0 .line 8,8 : 3,20 '' IL_0007: call float64 Program::Foo() IL_000c: stloc.1 .line 9,9 : 2,3 '' IL_000d: ret } // end of method Program::Main .method private hidebysig static int32 Foo() cil managed { // .maxstack 1 .locals init ([0] int32 V_0) .line 11,11 : 18,19 '' IL_0000: nop .line 12,12 : 3,12 '' IL_0001: ldc.i4.5 IL_0002: stloc.0 IL_0003: br.s IL_0005 .line 13,13 : 2,3 '' IL_0005: ldloc.0 IL_0006: ret } // end of method Program::Foo .method private hidebysig static float64 Foo() cil managed { // .maxstack 1 .locals init ([0] float64 V_0) .line 15,15 : 21,22 '' IL_0000: nop .line 16,16 : 3,15 '' IL_0001: ldc.r8 10. IL_000a: stloc.0 IL_000b: br.s IL_000d .line 17,17 : 2,3 '' IL_000d: ldloc.0 IL_000e: ret } // end of method Program::Bar .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 // ============================================================= // |
Interesting part is this:
1 2 3 4 |
IL_0001: call int32 Program::Foo() IL_0006: stloc.0 .line 8,8 : 3,20 '' IL_0007: call float64 Program::Foo() |
The way you call the method determines what methods you can declare since we need to be able to distinguish them. Since in IL you specify the return type when calling the method, you can have two methods with different return types.
28. What is the difference between const
and readonly
?
See this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; public class Program { const int a = 5; static readonly int b = 6; public static void Main() { Foo(a); Foo(b); } static void Foo(int x){ Console.WriteLine(x); } } |
When you decompile it you get:
1 2 3 4 5 6 |
IL_0001: ldc.i4.5 IL_0002: call void Program::Foo(int32) IL_0007: nop .line 10,10 : 3,10 '' IL_0008: ldsfld int32 Program::b IL_000d: call void Program::Foo(int32) |
Const variable was replaced with raw integer, readonly variable was just used. This may have very unexpected consequences — if you change the value of the const variable and recompile the code, but not the user of this code, then the caller will still use old value.
29. How are arguments passed in C#? By value? By reference? Differently?
Passing by value is easy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; public class Program { public static void Main() { int a = 5; Foo(a); Console.WriteLine(a); } static void Foo(int x){ x = 6; } } |
Output:
1 |
5 |
Here Foo(5)
passed the value. The same goes for all value types, they are passed by value so they are copied when passed to a function. Because of that the variable a
was not modified.
The same goes for references as well! You can often read that reference types are passed by, well, reference. But it is not true, see this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; public class Program { public static void Main() { object a = new object(); Foo(a); Console.WriteLine(a); } static void Foo(object x){ x = null; } } |
Output:
1 |
System.Object |
Even though reference was modified in the Foo
method, the variable a
is not modified. This is because what we pass is a reference, not an object. So reference types are passed in the same way as value types, only not the object is passed (or the value) but the reference to the object.
So to be super precise: references are passed by value but objects are passed by sharing.
C# can pass variables by reference as well:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; public class Program { public static void Main() { int a = 5; Foo(ref a); Console.WriteLine(a); } static void Foo(ref int x){ x = 6; } } |
Output:
1 |
6 |
When you execute it you can see that the variable a
was modified.
30. In what order are method parameters calculated?
Most of the times this doesn’t matter but for named parameters this can be a little tricky:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; public class Program { public static void Main() { Foo(b: Identity(5), a: Identity(4)); } static void Foo(int a, int b){ Console.WriteLine($"a = {a}\nb = {b}"); } static int Identity(int x){ Console.WriteLine(x); return x; } } |
Output:
1 2 3 4 |
5 4 a = 4 b = 5 |
Parameters are calculated in the order specified by the caller, not as specified in the function signature.
31. What is the difference between typeof
and GetType
?
The former is evaluated during compilation, the latter in runtime. This means that the former doesn’t need an object, as shown here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; public class Program { public static void Main() { Type t = typeof(Foo); Type u = new Foo().GetType(); } } class Foo{ public Foo(){ Console.WriteLine("Constructor!"); } } |
Decompile and get this:
1 2 3 4 5 6 |
IL_0001: ldtoken Foo IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) IL_000b: stloc.0 .line 8,8 : 3,32 '' IL_000c: newobj instance void Foo::.ctor() IL_0011: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType() |
32. What is behind using
and lock
? Was it always compiled in this way?
A typical try+finally pattern, like here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; public class Program { public static void Main() { lock(new object()){ } using(new Foo()){ } } } class Foo : IDisposable { public void Dispose(){} } |
For using
we get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
IL_0021: newobj instance void Foo::.ctor() IL_0026: stloc.2 .line 10,10 : 19,20 '' .try { IL_0027: nop .line 11,11 : 3,4 '' IL_0028: nop IL_0029: leave.s IL_0036 .line 16707566,16707566 : 0,0 '' } // end .try finally { IL_002b: ldloc.2 IL_002c: brfalse.s IL_0035 IL_002e: ldloc.2 IL_002f: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0034: nop IL_0035: endfinally .line 12,12 : 2,3 '' } // end handler |
So in finally we check if the object is not null and then we call Dispose
. It is important to notice that the object is created before the try
block!
For locking w get 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 |
IL_0001: newobj instance void [mscorlib]System.Object::.ctor() IL_0006: stloc.0 IL_0007: ldc.i4.0 IL_0008: stloc.1 .try { IL_0009: ldloc.0 IL_000a: ldloca.s V_1 IL_000c: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) IL_0011: nop .line 7,7 : 21,22 '' IL_0012: nop .line 8,8 : 3,4 '' IL_0013: nop IL_0014: leave.s IL_0021 .line 16707566,16707566 : 0,0 '' } // end .try finally { IL_0016: ldloc.1 IL_0017: brfalse.s IL_0020 IL_0019: ldloc.0 IL_001a: call void [mscorlib]System.Threading.Monitor::Exit(object) IL_001f: nop IL_0020: endfinally .line 10,10 : 3,19 '' } // end handler |
Again, object created before the try
block.
Before C# 4 lock was using Monitor.Enter(object)
method and was called before the try
block which could result in a nasty, never released lock. Read more here.
33. What is the .NET calling convention?
For x86 it is clrcall convention which is like Fastcall but passes arguments from left to right.
Take this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; public class Program { public static void Main() { Static(1, 2, 3, 4, 5, 6); new Program().Instance(1, 2, 3, 4, 5, 6); } public static void Static(int a, int b, int c, int d, int e, int f) { } public void Instance(int a, int b, int c, int d, int e, int f) { } } |
It gives these calls:
1 2 3 4 5 6 7 |
02b20465 6a03 push 3 02b20467 6a04 push 4 02b20469 6a05 push 5 02b2046b 6a06 push 6 02b2046d b901000000 mov ecx,1 02b20472 ba02000000 mov edx,2 02b20477 ff15484def00 call dword ptr ds:[0EF4D48h] (Program.Static(Int32, Int32, Int32, Int32, Int32, Int32), mdToken: 06000002) |
and
1 2 3 4 5 6 7 8 9 |
02b20494 6a02 push 2 02b20496 6a03 push 3 02b20498 6a04 push 4 02b2049a 6a05 push 5 02b2049c 6a06 push 6 02b2049e 8b4dfc mov ecx,dword ptr [ebp-4] 02b204a1 ba01000000 mov edx,1 02b204a6 ff15544def00 call dword ptr ds:[0EF4D54h] (Program.Instance(Int32, Int32, Int32, Int32, Int32, Int32), mdToken: 06000003) 02b204ac 90 nop |
For x64 it is Microsoft x64.
Code above gives:
1 2 3 4 5 6 7 |
00007ffa`820704a3 c744242005000000 mov dword ptr [rsp+20h],5 00007ffa`820704ab c744242806000000 mov dword ptr [rsp+28h],6 00007ffa`820704b3 b901000000 mov ecx,1 00007ffa`820704b8 ba02000000 mov edx,2 00007ffa`820704bd 41b803000000 mov r8d,3 00007ffa`820704c3 41b904000000 mov r9d,4 00007ffa`820704c9 e8b2fbffff call 00007ffa`82070080 (Program.Static(Int32, Int32, Int32, Int32, Int32, Int32), mdToken: 0000000006000002) |
and
1 2 3 4 5 6 7 8 |
00007ffa`820704eb c744242004000000 mov dword ptr [rsp+20h],4 00007ffa`820704f3 c744242805000000 mov dword ptr [rsp+28h],5 00007ffa`820704fb c744243006000000 mov dword ptr [rsp+30h],6 00007ffa`82070503 488b4df8 mov rcx,qword ptr [rbp-8] 00007ffa`82070507 ba01000000 mov edx,1 00007ffa`8207050c 41b802000000 mov r8d,2 00007ffa`82070512 41b903000000 mov r9d,3 00007ffa`82070518 e86bfbffff call 00007ffa`82070088 (Program.Instance(Int32, Int32, Int32, Int32, Int32, Int32), mdToken: 0000000006000003) |
34. Can you have const decimal
? const string
? const Foo
?
Yes, see here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; public class Program { const decimal a = 5; const string b = "abc"; const Foo c = null; public static void Main() { } } class Foo { } |
However, there is a trick here. If you decompile the code, you get 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 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 |
// Microsoft (R) .NET Framework IL Disassembler. Version 4.0.30319.33440 // Copyright (c) Microsoft Corporation. All rights reserved. // // 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 'bb0c0028-1e9e-407c-9c2f-43a28949efba' { .hash algorithm 0x00008004 .ver 0:0:0:0 } .module 'bb0c0028-1e9e-407c-9c2f-43a28949efba.dll' // MVID: {0AA5E085-683A-440A-A7C9-AFE31B57D6E0} .imagebase 0x10000000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000001 // ILONLY // Image base: 0x014C0000 // =============== CLASS MEMBERS DECLARATION =================== .class public auto ansi beforefieldinit Program extends [mscorlib]System.Object { .field private static initonly valuetype [mscorlib]System.Decimal a .field private static literal string b = "abc" .field private static literal class Foo c = nullref .method public hidebysig static void Main() cil managed { // .maxstack 8 .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}' .line 9,9 : 2,3 '' IL_0000: nop .line 10,10 : 2,3 '' IL_0001: 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 .method private hidebysig specialname rtspecialname static void .cctor() cil managed { // .maxstack 8 .line 5,5 : 2,22 '' IL_0000: ldc.i4.5 IL_0001: newobj instance void [mscorlib]System.Decimal::.ctor(int32) IL_0006: stsfld valuetype [mscorlib]System.Decimal Program::a IL_000b: ret } // end of method Program::.cctor } // end of class Program .class private auto ansi beforefieldinit Foo extends [mscorlib]System.Object { .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 Foo::.ctor } // end of class Foo // ============================================================= // |
Interesting parts are:
1 |
.field private static initonly valuetype [mscorlib]System.Decimal a |
1 2 3 4 5 6 7 8 9 10 11 |
.method private hidebysig specialname rtspecialname static void .cctor() cil managed { // .maxstack 8 .line 5,5 : 2,22 '' IL_0000: ldc.i4.5 IL_0001: newobj instance void [mscorlib]System.Decimal::.ctor(int32) IL_0006: stsfld valuetype [mscorlib]System.Decimal Program::a IL_000b: ret } // end of method Program::.cctor |
So you can see that the decimal
is not a constant but readonly
, it is initialized in the type constructor.
35. Can you pass a volatile
variable by reference?
Yes and no. This code compiles:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System; public class Program { static volatile int a = 5; public static void Main() { Foo(ref a); } static void Foo(ref int x){ } } |
But it shows warning that 'Program.a': a reference to a volatile field will not be treated as volatile
.