But the strange thing is, the restriction is not open. The Tuple class does not have this restriction specified in its generic type parameter TRest. So you can even declare the last argument as string, but when your application actually tries to create an object of it, the application will throw an ArgumentException.
This seemed to be very weird to me. I was thinking why did Microsoft did not put this in Type argument, making ITuple (an internal implementation) public, or create a special sealed class, and expose the same as a constraint to Type argument. I think this could be answered only by BCL Team.
Download Sample Code - 30KB
The Test
In this post, I will try to test the Two scenario :
- The performance between the two type of implementation, one with constraint given on Generic Type parameter directly and another with no constructor parameter but with a check in its constructor.
- The demonstration in terms of IL.
Well to do this, I have built two classes, and one Interface. I will try to give restriction to my class based on the Interface defined such that the second argument must be of that type. Now lets look at the implementation :
1. IMyType : A dummy interface.
public interface IMyType { string Sample1 { get; set; } }
2. MyType1<T1,T2>
public class MyType1<T1, T2> : IMyType { public T1 Item1 { get; set; } public T2 Item2 { get; set; } public MyType1(T1 t1, T2 t2) { if(!(t2 is IMyType)) throw new ArgumentException("Exception in argument list"); this.Item1 = t1; this.Item2 = t2; } #region IMyType Members public string Sample1 { get; set; } #endregion }
So the class MyType1 actually puts the restriction from the constructor itself with a checking of its type.
3. MyType2<T1,T2>
public class MyType2<T1, T2> : IMyType where T2: IMyType { public T1 Item1 { get; set; } public T2 Item2 { get; set; } public MyType2(T1 t1, T2 t2) { this.Item1 = t1; this.Item2 = t2; } #region IMyType Members public string Sample1 { get; set; } #endregion }
In this implementation, the actual restriction is given from outside using Generic Type constraint.
Now lets run a test on both the classes to see which one performs well.
For simplicity purpose, I am just creating the objects of both the types and checking the time taken to create each of those objects:
Console.WriteLine("CurrentTime is : {0}", DateTime.Now.ToLongTimeString()); DateTime currentTime = DateTime.Now; for (int i = 0; i < 20000000; i++) { MyType1<string, MyDummyType> typ = new MyType1<string, MyDummyType>("thisObject", new MyDummyType()); typ = null; } TimeSpan span = DateTime.Now.Subtract(currentTime); Console.WriteLine("Created 2 crores of type1 : {0}, Time Elapsed : {1} ", DateTime.Now.ToLongTimeString(), span.Milliseconds); currentTime = DateTime.Now; for (int i = 0; i < 20000000; i++) { MyType2<string, MyDummyType> typ = new MyType2<string, MyDummyType>("thisObject", new MyDummyType()); typ = null; } span = DateTime.Now.Subtract(currentTime); Console.WriteLine("Created 2 crores of type1 : {0}, Time Elapsed : {1} ", DateTime.Now.ToLongTimeString(), span.Milliseconds); Console.ReadLine();
So when I run my code, it seems to that the one with Generic constraint runs half of time than the other one. So MyType2 perform better than MyType1.
And rightly so, as I thought. Hmm, actually if you look into the implementation of both of the classes, you can definitely see that MyType1 will have more IL expressions in its constructor than MyType2. When I create MyType1 for 2 crore times, it takes 843 milliseconds while the second takes only 359 milliseconds. So it is almost less than half of the time on creating these types.
IL for the constructors:
Now if you see the IL of both the constructor, it will look :
1. MyType1<T1,T2>
.method public hidebysig specialname rtspecialname instance void .ctor(!T1 t1, !T2 t2) cil managed { // Code size 55 (0x37) .maxstack 2 .locals init ([0] bool CS$4$0000) IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: nop IL_0008: ldarg.2 IL_0009: box !T2 IL_000e: isinst StressTest.IMyType IL_0013: ldnull IL_0014: cgt.un IL_0016: stloc.0 IL_0017: ldloc.0 IL_0018: brtrue.s IL_0025 IL_001a: ldstr "Exception in argument list" IL_001f: newobj instance void [mscorlib]System.ArgumentException::.ctor(string) IL_0024: throw IL_0025: ldarg.0 IL_0026: ldarg.1 IL_0027: call instance void class StressTest.MyType1`2<!T1,!T2>::set_Item1(!0) IL_002c: nop IL_002d: ldarg.0 IL_002e: ldarg.2 IL_002f: call instance void class StressTest.MyType1`2<!T1,!T2>::set_Item2(!1) IL_0034: nop IL_0035: nop IL_0036: ret }
2. MyType2<T1,T2>
.method public hidebysig specialname rtspecialname instance void .ctor(!T1 t1, !T2 t2) cil managed { // Code size 26 (0x1a) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: nop IL_0008: ldarg.0 IL_0009: ldarg.1 IL_000a: call instance void class StressTest.MyType2`2<!T1,!T2>::set_Item1(!0) IL_000f: nop IL_0010: ldarg.0 IL_0011: ldarg.2 IL_0012: call instance void class StressTest.MyType2`2<!T1,!T2>::set_Item2(!1) IL_0017: nop IL_0018: nop IL_0019: ret }
So basically if you see the IL for both the cases, it seems the former is putting one IsInst expression to check and also creates one boolean variable to store the operation result.
Inference
Hence, basically, as everyone might thought, it is better to use Type constraint in generic argument. It is better to create a class or an interface, when you want your type to follow the Type parameter and put the constraint directly in the Type using where T:classname. In the above test, the former fails in terms or performance as the former builds up more IL and hence the constructor logic takes more time.
Download Sample Code - 30KB
Thanks for reading my post.
No comments:
Post a Comment
Please make sure that the question you ask is somehow related to the post you choose. Otherwise you post your general question in Forum section.