This is the first part of the Traits in C# series. For your convenience you can find other parts using the links below :
Part 1 — Basic implementation with Fody
Part 2 — Overrides
Part 3 — Stacking traits
If you are interested in the topic see the
article from „Implementacja traitów w języku C# przy użyciu biblioteki Fody” in Programista nr 55
Hi! This post starts new series about implementing traits in C#. Let’s go.
Table of Contents
What is that?
Wikipedia says that “trait is a concept used in object-oriented programming, which represents a set of methods that can be used to extend the functionality of a class”. It allows us to reuse existing functions in new types. Trait is something between class and interface — single class can have multiple traits (“inherit” from them), so this is similar to interfaces (since we can implement multiple interfaces in C# class), and each trait can have multiple methods with implementation (just like method in a class). However, traits are stateless — they cannot provide variables, only methods.
As a side note — stating that C# doesn’t support multiple inheritance is incorrect. We cannot inherit from multiple classes, however, we can implement (inherit from) multiple interfaces. It is a good moment to distinct between interface inheritance and implementation inheritance. The former means that we are allowed to gather interface (method signatures) from different types, the latter indicates that we can reuse actual implementation. There are languages supporting inheritance from multiple classes — C++ is an epitomy here. It is also worth noting that there is another important concept called mix—in. Actually, Scala’s traits are in fact mix-ins.
Now you might ask — why do I ever need this kind of a thing? Well, it’s difficult question. You might very well never need to use the concept, but there are situations when it would be very helpful to be able to define trait. Imagine the case: we have IEnumerable
with method Select
. We have also IQueryable
which has the same method (to be precise: method with the same signature). But if you try to do something like this:
1 2 |
IEnumerable< Foo> foos = SomethingReturningIQueryableOfFoos(); foos.Select(f => f.Something); |
you will actually call Select
from IEnumerable
instead of IQueryable
, even though foos
is the latter. This is because of early binding — Select
is just an extension method and it is bound during compilation. However, let’s take this Java 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 |
import java.util.*; import java.lang.*; import java.io.*; interface Interface1 { default void print(){ System.out.println("I am Interface1"); } } interface Interface2 extends Interface1 { default void print(){ System.out.println("I am Interface2"); } } class Class1 implements Interface2 { } class Ideone { public static void main (String[] args) throws java.lang.Exception { Interface1 instance = new Class1(); instance.print(); } } |
This will actually print “I am Interface2”, even though instance
variable has compilation type Interface1
. If you try to reimplement this snippet in C# using extension methods, you will get different results.
As we can see, trait is pretty powerful concept. Unfortunately, C# doesn’t support them. In this series we are going to impement very basic Fody module supporting traits.
Usage
Let’s write down what we want to achieve:
- We would like to be able to define a trait
- We would like to have intellisense — we still want to have compiler support when writing code
- We want traits to be polymorphic — they need to support late binding
- For sake of simplicity we won’t bother with fancy functions — we will just support parameterless void ones
- As usual, we want the process to be clean and reliable
We need to have methods’ implementations somewhere. Since we want to have compiler’s support, we will use extension methods. Basically, our code will look like 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 |
using System; using TraitIntroducer; namespace TraitsDemo { public interface IA { } [TraitFor(typeof(IA))] public static class IA_Implementation { public static void Print( this IA instance) { Console.WriteLine( "I'm IA"); } } public class A : IA { } public class B : A { public virtual void Print() { Console.WriteLine( "I'm B"); } } class Program { static void Main( string[] args) { IA ia = new B(); ia.Print(); } } } |
If you compile this code as-is, it will print “I’m IA”. Why? Because ia
variable is of type IA
during compilation, and ia.Print()
is bound to static extension method. However, we would like this to be transformed to the following:
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 |
using System; using TraitIntroducer; namespace TraitsDemo { public interface IA { void Print(); } [TraitFor(typeof(IA))] public static class IA_Implementation { public static void Print( this IA instance) { instance.Print(); } } public class A : IA { public virtual void Print( this IA instance) { Console.WriteLine( "I'm IA"); } } public class B : A { public virtual override void Print() { Console.WriteLine( "I'm B"); } } class Program { static void Main( string[] args) { IA ia = new B(); ia.Print(); } } } |
So we introduce method Print
to interface IA
, we move original implementation from extension method to newly created method in class A
, and we modify existing extension method to call method from interface. What’s more, since ia
is actually of type B
which overrides method Print
, we should get different result. This time (thanks to late binding) we expect to get “I’m B”.
Implementation
As we said, we are going to utilize Fody to modify the code. We need to mark classes providing actual implementation in order to be able to find them in weaver. So we start with the following attribute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; namespace TraitIntroducer { public class TraitForAttribute : Attribute { public Type InterfaceType { get; private set; } public TraitForAttribute(Type interfaceType) { InterfaceType = interfaceType; } } } |
This attribute accepts one parameter — type of interface which we want to boost. Using this attribute we will write the code in the following way:
- We define empty interface which we will fill up with methods using Fody
- Methods which we would like to be implemented in interface we will implement as extension methods — thanks to this we will have intellisense
- We will handle rest using Fody
Let’s dig into Fody’s module. We start with the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void Execute() { var allTypes = ModuleDefinition.Types.Concat(ModuleDefinition.GetTypeReferences().Select(t => t.Resolve())).ToArray(); var traitForAttributeTypeDefinition = allTypes.FirstOrDefault(t => t.FullName == typeof(TraitForAttribute).FullName); if (traitForAttributeTypeDefinition == null) return; IDictionary< TypeDefinition, IList< MethodDefinition>> introducedMethods = GetIntroducedMethods(allTypes, traitForAttributeTypeDefinition); foreach (var tuple in introducedMethods) { ExtendInterface(tuple.Key, tuple.Value); ExtendInheritors(tuple.Key, tuple.Value, allTypes); FixTraitMethods(tuple.Key, tuple.Value); } } |
First, we get all types in order to examine them. Next, we look for attribute’s metadata. If we can’t find it, it probably means that it is not used, so we are done. If it is used, we group all introduced methods (extension methods) by type of their target interface. For each such method we first extend interface (which means copying extension method signature to the interface), we extend inheritors (which means that we copy extension methods to classes), and fix trait methods (which means that we modify extension methods to call methods from interfaces).
Let’s see how we extract methods to introduce to interfaces:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private IDictionary< TypeDefinition, IList< MethodDefinition>> GetIntroducedMethods(TypeDefinition[] allTypes, TypeDefinition traitForAttributeTypeDefinition) { var result = new Dictionary< TypeDefinition, IList< MethodDefinition>>(); var implementers = allTypes.Where(type => type.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == traitForAttributeTypeDefinition.FullName)); foreach(var implementer in implementers) { var extendedInterafaceType = implementer.CustomAttributes.First(attribute => attribute.AttributeType.FullName == traitForAttributeTypeDefinition.FullName).ConstructorArguments.First().Value as TypeDefinition; var extendedInterfaceTypeDefinition = allTypes.First(type => type.FullName == extendedInterafaceType.FullName); var extendingMethods = implementer.Methods; result[extendedInterfaceTypeDefinition] = extendingMethods.ToList(); } return result; } |
We iterate over all classes marked with our attribute. For each class we extract type from attribute indicated which interface we want to boost. Next, we take all methods from class and store them in the dictionary. Her we implicitly assume that there is only one class with extension methods for particular interface, but it is not difficult to change this behaviour.
Let’s see how we extend interfaces:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private void ExtendInterface(TypeDefinition extendedInterface, IList< MethodDefinition> methods) { foreach (var method in methods) { InjectMethodToInterface(extendedInterface, method); } } private void InjectMethodToInterface(TypeDefinition extendedInterface, MethodDefinition method) { var newMethod = new MethodDefinition(method.Name, MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot, method.ReturnType); extendedInterface.Methods.Add(newMethod); } |
For each interface and for each extension method we create new method (with specific attributes) and simply add this method. Methods are empty so we don’t need to bother with body here.
You might ask: how do you know which attributes should you use? If you look into MethodAttributes
in Mono.Cecil you will find out that there are lots of them. Well, it is pretty simple: just create interface with a method, compile the code, and then decompile it using ILSpy or any other tool — you will see all attributes which probably are needed.
OK, we know how to extend interface. Let’s see how to inject implementation to concrete classes:
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 |
private void ExtendInheritors(TypeDefinition extendedType, IList< MethodDefinition> methods, TypeDefinition[] allTypes) { var inheritors = GetInheritingTypes(extendedType, allTypes); foreach (var inheritor in inheritors) { ExtendInheritor(inheritor, extendedType, methods, allTypes); } } private void ExtendInheritor(TypeDefinition inheritor, TypeDefinition extendedType, IList< MethodDefinition> methods, TypeDefinition[] allTypes) { foreach (var method in methods) { InjectMethodToInheritor(inheritor, extendedType, method, allTypes); } } private void InjectMethodToInheritor(TypeDefinition inheritor, TypeDefinition extendedType, MethodDefinition method, TypeDefinition[] allTypes) { var newMethod = new MethodDefinition(method.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.NewSlot, method.ReturnType); foreach (var instruction in method.Body.Instructions) { newMethod.Body.Instructions.Add(instruction); } inheritor.Methods.Add(newMethod); FixDescendants(inheritor, allTypes, method); } private void FixDescendants(TypeDefinition inheritor, TypeDefinition[] allTypes, MethodDefinition method) { var primaryDescendants = allTypes.Where(type => type.BaseType == inheritor); foreach (var descendant in primaryDescendants) { foreach (var methodToFix in descendant.Methods.Where(descendantMethod => descendantMethod.Name == method.Name)) { methodToFix.Attributes |= MethodAttributes.Virtual; methodToFix.Attributes &= ~(MethodAttributes.NewSlot); } FixDescendants(descendant, allTypes, method); } } |
We iterate over all types, over all methods, and create new methods. We do almost the same as when modifying interfaces, but this time we need to add body to methods (and also please note that methods’ attributes are different this time). Since we want to copy extension methods exactly, we simply iterate over all instructions and add them one by one. Finally, we need to fix subtypes — each subtype is allowed to define method with the same signature which does not need to be marked as virtual. We need to take care of that in order to have polymorphic invocation. In order to do that, we simply add attribute Virtual
to each method, and remove attribute NewSlot
. The former should be pretty obvious. The latter means that this method hides method with the same signature in the base class. If we remove this attribute, polymorphic invocation will work.
Finally, let’s see how to fix the extension methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private void FixTraitMethods(TypeDefinition extendedInterface, IList< MethodDefinition> methods) { foreach (var method in methods) { FixTraitMethod(extendedInterface, method); } } private void FixTraitMethod(TypeDefinition extendedInterface, MethodDefinition method) { var methodToCall = extendedInterface.Methods.First(interfaceMethod => interfaceMethod.Name == method.Name); LogWarning(methodToCall.FullName); method.Body.Instructions.Clear(); method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); method.Body.Instructions.Add(Instruction.Create(OpCodes.Callvirt, methodToCall)); method.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); } |
This time we need to write some magic code. Since we need to call method from interface, we first find its definition. Next, we add IL code which performs the invocation. Since we assume that methods are parameterless and void, we only need to push instance on the stack and call the method. That’s all.
Test code and results
Let’s compile our demo program with Fody enabled and see final IL (using DotPeek):
Demo program:
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 |
// Type: TraitsDemo.Program // Assembly: TraitsDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 884502CE-D2C9-4FE4-BF73-324C59278A5B // Location: C:\Adam\TraitsDemo\bin\Debug\TraitsDemo.exe // Sequence point data from decompiler .class private auto ansi beforefieldinit TraitsDemo.Program extends [mscorlib]System.Object { .method private hidebysig static void Main( string[] args ) cil managed { .entrypoint .maxstack 1 .locals init ( [0] class TraitsDemo.IA V_0 ) IL_0000: nop // [13 7 - 13 39] IL_0001: newobj instance void TraitsDemo.B::.ctor() IL_0006: stloc.0 // V_0 IL_0007: ldloc.0 // V_0 IL_0008: call void TraitsDemo.IA_Implementation::Print(class TraitsDemo.IA) IL_000d: nop IL_000e: ret } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000: ldarg.0 // this IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class TraitsDemo.Program |
Class A
:
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 |
// Type: TraitsDemo.A // Assembly: TraitsDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 884502CE-D2C9-4FE4-BF73-324C59278A5B // Location: C:\Adam\TraitsDemo\bin\Debug\TraitsDemo.exe // Sequence point data from decompiler .class public auto ansi beforefieldinit TraitsDemo.A extends [mscorlib]System.Object implements TraitsDemo.IA { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000: ldarg.0 // this IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method A::.ctor .method public hidebysig virtual newslot instance void Print() cil managed { .maxstack 8 IL_0000: nop // [15 7 - 15 34] IL_0001: ldstr "I'm IA" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method A::Print } // end of class TraitsDemo.A |
Interface IA
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Type: TraitsDemo.IA // Assembly: TraitsDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 884502CE-D2C9-4FE4-BF73-324C59278A5B // Location: C:\Adam\bin\Debug\TraitsDemo.exe // Sequence point data from decompiler .class interface public abstract auto ansi TraitsDemo.IA { .method compilercontrolled hidebysig virtual newslot abstract instance void Print() cil managed { // Can't find a body } // end of method IA::Print } // end of class TraitsDemo.IA |
Class IA_Implementation
:
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 |
// Type: TraitsDemo.IA_Implementation // Assembly: TraitsDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 884502CE-D2C9-4FE4-BF73-324C59278A5B // Location: C:\Adam\TraitsDemo\bin\Debug\TraitsDemo.exe // Sequence point data from decompiler .class public abstract sealed auto ansi beforefieldinit TraitsDemo.IA_Implementation extends [mscorlib]System.Object { .custom instance void [TraitIntroducer]TraitIntroducer.TraitForAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 0d 54 72 61 69 74 73 44 65 6d 6f 2e 49 41 // ...TraitsDemo.IA 00 00 // .. ) // MetadataClassType(TraitsDemo.IA) .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (01 00 00 00 ) .method public hidebysig static void Print( class TraitsDemo.IA 'instance' ) cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [16 7 - 16 23] IL_0000: ldarg.0 // 'instance' IL_0001: callvirt instance void TraitsDemo.IA::Print() IL_0006: ret } // end of method IA_Implementation::Print } // end of class TraitsDemo.IA_Implementation |
Looks pretty decent — and it works.
Summary
This is the end of part one. Next time we are going to write some code to analyse inheritance hierarchy in order to be able to override trait methods in interfaces, so we will have Print
in IA
and IB : IA
, and we will copy correct methods to concrete classes. See you next time!