Seit einiger Zeit denke ich über zeitnahe Einschränkungen nach, daher ist dies eine perfekte Gelegenheit, um das Konzept auf den Markt zu bringen.
Die Grundidee ist, dass Sie, wenn Sie keine Überprüfungskompilierungszeit durchführen können, dies zum frühestmöglichen Zeitpunkt tun sollten. Dies ist im Grunde der Moment, in dem die Anwendung gestartet wird. Wenn alle Überprüfungen in Ordnung sind, wird die Anwendung ausgeführt. Wenn eine Prüfung fehlschlägt, schlägt die Anwendung sofort fehl.
Verhalten
Das bestmögliche Ergebnis ist, dass unser Programm nicht kompiliert wird, wenn die Einschränkungen nicht erfüllt sind. Leider ist dies in der aktuellen C # -Implementierung nicht möglich.
Das nächstbeste ist, dass das Programm in dem Moment abstürzt, in dem es gestartet wird.
Die letzte Option ist, dass das Programm abstürzt, sobald der Code getroffen wird. Dies ist das Standardverhalten von .NET. Für mich ist das völlig inakzeptabel.
Voraussetzungen
Wir brauchen einen Beschränkungsmechanismus, also mangels etwas Besserem ... verwenden wir ein Attribut. Das Attribut wird zusätzlich zu einer generischen Einschränkung angezeigt, um zu überprüfen, ob es unseren Bedingungen entspricht. Wenn nicht, geben wir einen hässlichen Fehler.
Dies ermöglicht es uns, solche Dinge in unserem Code zu tun:
public class Clas<[IsInterface] T> where T : class
(Ich habe das where T:class
hier beibehalten , weil ich immer Überprüfungen zur Kompilierungszeit gegenüber Überprüfungen zur Laufzeit bevorzuge.)
Wir haben also nur ein Problem: Wir prüfen, ob alle von uns verwendeten Typen mit der Einschränkung übereinstimmen. Wie schwer kann es sein?
Lass es uns aufbrechen
Generische Typen befinden sich immer entweder in einer Klasse (/ struct / interface) oder in einer Methode.
Um eine Einschränkung auszulösen, müssen Sie eines der folgenden Dinge tun:
- Kompilierungszeit bei Verwendung eines Typs in einem Typ (Vererbung, generische Einschränkung, Klassenmitglied)
- Kompilierungszeit, wenn ein Typ in einem Methodenkörper verwendet wird
- Laufzeit, wenn Reflection verwendet wird, um etwas basierend auf der generischen Basisklasse zu erstellen.
- Laufzeit, wenn mithilfe von Reflektion etwas basierend auf RTTI erstellt wird.
An dieser Stelle möchte ich darauf hinweisen, dass Sie es immer vermeiden sollten, (4) in einem IMO-Programm zu tun. Unabhängig davon werden diese Überprüfungen dies nicht unterstützen, da dies effektiv die Lösung des Stoppproblems bedeuten würde.
Fall 1: Verwenden eines Typs
Beispiel:
public class TestClass : SomeClass<IMyInterface> { ... }
Beispiel 2:
public class TestClass
{
SomeClass<IMyInterface> myMember; // or a property, method, etc.
}
Grundsätzlich umfasst dies das Scannen aller Typen, Vererbungen, Elemente, Parameter usw. usw. usw. Wenn ein Typ ein generischer Typ ist und eine Einschränkung aufweist, überprüfen wir die Einschränkung. Wenn es sich um ein Array handelt, überprüfen wir den Elementtyp.
An dieser Stelle muss ich hinzufügen, dass dies die Tatsache zunichte macht, dass .NET standardmäßig die Typen 'faul' lädt. Durch das Scannen aller Typen erzwingen wir, dass die .NET-Laufzeit alle lädt. Für die meisten Programme sollte dies kein Problem sein. Wenn Sie jedoch statische Initialisierer in Ihrem Code verwenden, können Probleme mit diesem Ansatz auftreten ... Trotzdem würde ich niemandem raten, dies zu tun (außer solchen Dingen :-), also sollte es nicht geben Sie viele Probleme.
Fall 2: Verwenden eines Typs in einer Methode
Beispiel:
void Test() {
new SomeClass<ISomeInterface>();
}
Um dies zu überprüfen, haben wir nur eine Option: Dekompilieren Sie die Klasse, überprüfen Sie alle verwendeten Mitgliedstoken und überprüfen Sie die Argumente, wenn eines davon der generische Typ ist.
Fall 3: Reflexion, generische Laufzeitkonstruktion
Beispiel:
typeof(CtorTest<>).MakeGenericType(typeof(IMyInterface))
Ich nehme an, es ist theoretisch möglich, dies mit ähnlichen Tricks wie in Fall (2) zu überprüfen, aber die Implementierung ist viel schwieriger (Sie müssen überprüfen, ob MakeGenericType
in einem Codepfad aufgerufen wird). Ich werde hier nicht auf Details eingehen ...
Fall 4: Reflexion, Laufzeit RTTI
Beispiel:
Type t = Type.GetType("CtorTest`1[IMyInterface]");
Dies ist das Worst-Case-Szenario und, wie ich zuvor erklärt habe, meiner Meinung nach generell eine schlechte Idee. In beiden Fällen gibt es keine praktische Möglichkeit, dies mithilfe von Schecks herauszufinden.
Das Los testen
Das Erstellen eines Programms, das die Fälle (1) und (2) testet, führt zu folgenden Ergebnissen:
[AttributeUsage(AttributeTargets.GenericParameter)]
public class IsInterface : ConstraintAttribute
{
public override bool Check(Type genericType)
{
return genericType.IsInterface;
}
public override string ToString()
{
return "Generic type is not an interface";
}
}
public abstract class ConstraintAttribute : Attribute
{
public ConstraintAttribute() {}
public abstract bool Check(Type generic);
}
internal class BigEndianByteReader
{
public BigEndianByteReader(byte[] data)
{
this.data = data;
this.position = 0;
}
private byte[] data;
private int position;
public int Position
{
get { return position; }
}
public bool Eof
{
get { return position >= data.Length; }
}
public sbyte ReadSByte()
{
return (sbyte)data[position++];
}
public byte ReadByte()
{
return (byte)data[position++];
}
public int ReadInt16()
{
return ((data[position++] | (data[position++] << 8)));
}
public ushort ReadUInt16()
{
return (ushort)((data[position++] | (data[position++] << 8)));
}
public int ReadInt32()
{
return (((data[position++] | (data[position++] << 8)) | (data[position++] << 0x10)) | (data[position++] << 0x18));
}
public ulong ReadInt64()
{
return (ulong)(((data[position++] | (data[position++] << 8)) | (data[position++] << 0x10)) | (data[position++] << 0x18) |
(data[position++] << 0x20) | (data[position++] << 0x28) | (data[position++] << 0x30) | (data[position++] << 0x38));
}
public double ReadDouble()
{
var result = BitConverter.ToDouble(data, position);
position += 8;
return result;
}
public float ReadSingle()
{
var result = BitConverter.ToSingle(data, position);
position += 4;
return result;
}
}
internal class ILDecompiler
{
static ILDecompiler()
{
// Initialize our cheat tables
singleByteOpcodes = new OpCode[0x100];
multiByteOpcodes = new OpCode[0x100];
FieldInfo[] infoArray1 = typeof(OpCodes).GetFields();
for (int num1 = 0; num1 < infoArray1.Length; num1++)
{
FieldInfo info1 = infoArray1[num1];
if (info1.FieldType == typeof(OpCode))
{
OpCode code1 = (OpCode)info1.GetValue(null);
ushort num2 = (ushort)code1.Value;
if (num2 < 0x100)
{
singleByteOpcodes[(int)num2] = code1;
}
else
{
if ((num2 & 0xff00) != 0xfe00)
{
throw new Exception("Invalid opcode: " + num2.ToString());
}
multiByteOpcodes[num2 & 0xff] = code1;
}
}
}
}
private ILDecompiler() { }
private static OpCode[] singleByteOpcodes;
private static OpCode[] multiByteOpcodes;
public static IEnumerable<ILInstruction> Decompile(MethodBase mi, byte[] ildata)
{
Module module = mi.Module;
BigEndianByteReader reader = new BigEndianByteReader(ildata);
while (!reader.Eof)
{
OpCode code = OpCodes.Nop;
int offset = reader.Position;
ushort b = reader.ReadByte();
if (b != 0xfe)
{
code = singleByteOpcodes[b];
}
else
{
b = reader.ReadByte();
code = multiByteOpcodes[b];
b |= (ushort)(0xfe00);
}
object operand = null;
switch (code.OperandType)
{
case OperandType.InlineBrTarget:
operand = reader.ReadInt32() + reader.Position;
break;
case OperandType.InlineField:
if (mi is ConstructorInfo)
{
operand = module.ResolveField(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes);
}
else
{
operand = module.ResolveField(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments());
}
break;
case OperandType.InlineI:
operand = reader.ReadInt32();
break;
case OperandType.InlineI8:
operand = reader.ReadInt64();
break;
case OperandType.InlineMethod:
try
{
if (mi is ConstructorInfo)
{
operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes);
}
else
{
operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments());
}
}
catch
{
operand = null;
}
break;
case OperandType.InlineNone:
break;
case OperandType.InlineR:
operand = reader.ReadDouble();
break;
case OperandType.InlineSig:
operand = module.ResolveSignature(reader.ReadInt32());
break;
case OperandType.InlineString:
operand = module.ResolveString(reader.ReadInt32());
break;
case OperandType.InlineSwitch:
int count = reader.ReadInt32();
int[] targetOffsets = new int[count];
for (int i = 0; i < count; ++i)
{
targetOffsets[i] = reader.ReadInt32();
}
int pos = reader.Position;
for (int i = 0; i < count; ++i)
{
targetOffsets[i] += pos;
}
operand = targetOffsets;
break;
case OperandType.InlineTok:
case OperandType.InlineType:
try
{
if (mi is ConstructorInfo)
{
operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes);
}
else
{
operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments());
}
}
catch
{
operand = null;
}
break;
case OperandType.InlineVar:
operand = reader.ReadUInt16();
break;
case OperandType.ShortInlineBrTarget:
operand = reader.ReadSByte() + reader.Position;
break;
case OperandType.ShortInlineI:
operand = reader.ReadSByte();
break;
case OperandType.ShortInlineR:
operand = reader.ReadSingle();
break;
case OperandType.ShortInlineVar:
operand = reader.ReadByte();
break;
default:
throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType);
}
yield return new ILInstruction(offset, code, operand);
}
}
}
public class ILInstruction
{
public ILInstruction(int offset, OpCode code, object operand)
{
this.Offset = offset;
this.Code = code;
this.Operand = operand;
}
public int Offset { get; private set; }
public OpCode Code { get; private set; }
public object Operand { get; private set; }
}
public class IncorrectConstraintException : Exception
{
public IncorrectConstraintException(string msg, params object[] arg) : base(string.Format(msg, arg)) { }
}
public class ConstraintFailedException : Exception
{
public ConstraintFailedException(string msg) : base(msg) { }
public ConstraintFailedException(string msg, params object[] arg) : base(string.Format(msg, arg)) { }
}
public class NCTChecks
{
public NCTChecks(Type startpoint)
: this(startpoint.Assembly)
{ }
public NCTChecks(params Assembly[] ass)
{
foreach (var assembly in ass)
{
assemblies.Add(assembly);
foreach (var type in assembly.GetTypes())
{
EnsureType(type);
}
}
while (typesToCheck.Count > 0)
{
var t = typesToCheck.Pop();
GatherTypesFrom(t);
PerformRuntimeCheck(t);
}
}
private HashSet<Assembly> assemblies = new HashSet<Assembly>();
private Stack<Type> typesToCheck = new Stack<Type>();
private HashSet<Type> typesKnown = new HashSet<Type>();
private void EnsureType(Type t)
{
// Don't check for assembly here; we can pass f.ex. System.Lazy<Our.T<MyClass>>
if (t != null && !t.IsGenericTypeDefinition && typesKnown.Add(t))
{
typesToCheck.Push(t);
if (t.IsGenericType)
{
foreach (var par in t.GetGenericArguments())
{
EnsureType(par);
}
}
if (t.IsArray)
{
EnsureType(t.GetElementType());
}
}
}
private void PerformRuntimeCheck(Type t)
{
if (t.IsGenericType && !t.IsGenericTypeDefinition)
{
// Only check the assemblies we explicitly asked for:
if (this.assemblies.Contains(t.Assembly))
{
// Gather the generics data:
var def = t.GetGenericTypeDefinition();
var par = def.GetGenericArguments();
var args = t.GetGenericArguments();
// Perform checks:
for (int i = 0; i < args.Length; ++i)
{
foreach (var check in par[i].GetCustomAttributes(typeof(ConstraintAttribute), true).Cast<ConstraintAttribute>())
{
if (!check.Check(args[i]))
{
string error = "Runtime type check failed for type " + t.ToString() + ": " + check.ToString();
Debugger.Break();
throw new ConstraintFailedException(error);
}
}
}
}
}
}
// Phase 1: all types that are referenced in some way
private void GatherTypesFrom(Type t)
{
EnsureType(t.BaseType);
foreach (var intf in t.GetInterfaces())
{
EnsureType(intf);
}
foreach (var nested in t.GetNestedTypes())
{
EnsureType(nested);
}
var all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
foreach (var field in t.GetFields(all))
{
EnsureType(field.FieldType);
}
foreach (var property in t.GetProperties(all))
{
EnsureType(property.PropertyType);
}
foreach (var evt in t.GetEvents(all))
{
EnsureType(evt.EventHandlerType);
}
foreach (var ctor in t.GetConstructors(all))
{
foreach (var par in ctor.GetParameters())
{
EnsureType(par.ParameterType);
}
// Phase 2: all types that are used in a body
GatherTypesFrom(ctor);
}
foreach (var method in t.GetMethods(all))
{
if (method.ReturnType != typeof(void))
{
EnsureType(method.ReturnType);
}
foreach (var par in method.GetParameters())
{
EnsureType(par.ParameterType);
}
// Phase 2: all types that are used in a body
GatherTypesFrom(method);
}
}
private void GatherTypesFrom(MethodBase method)
{
if (this.assemblies.Contains(method.DeclaringType.Assembly)) // only consider methods we've build ourselves
{
MethodBody methodBody = method.GetMethodBody();
if (methodBody != null)
{
// Handle local variables
foreach (var local in methodBody.LocalVariables)
{
EnsureType(local.LocalType);
}
// Handle method body
var il = methodBody.GetILAsByteArray();
if (il != null)
{
foreach (var oper in ILDecompiler.Decompile(method, il))
{
if (oper.Operand is MemberInfo)
{
foreach (var type in HandleMember((MemberInfo)oper.Operand))
{
EnsureType(type);
}
}
}
}
}
}
}
private static IEnumerable<Type> HandleMember(MemberInfo info)
{
// Event, Field, Method, Constructor or Property.
yield return info.DeclaringType;
if (info is EventInfo)
{
yield return ((EventInfo)info).EventHandlerType;
}
else if (info is FieldInfo)
{
yield return ((FieldInfo)info).FieldType;
}
else if (info is PropertyInfo)
{
yield return ((PropertyInfo)info).PropertyType;
}
else if (info is ConstructorInfo)
{
foreach (var par in ((ConstructorInfo)info).GetParameters())
{
yield return par.ParameterType;
}
}
else if (info is MethodInfo)
{
foreach (var par in ((MethodInfo)info).GetParameters())
{
yield return par.ParameterType;
}
}
else if (info is Type)
{
yield return (Type)info;
}
else
{
throw new NotSupportedException("Incorrect unsupported member type: " + info.GetType().Name);
}
}
}
Den Code verwenden
Nun, das ist der einfache Teil :-)
// Create something illegal
public class Bar2 : IMyInterface
{
public void Execute()
{
throw new NotImplementedException();
}
}
// Our fancy check
public class Foo<[IsInterface] T>
{
}
class Program
{
static Program()
{
// Perform all runtime checks
new NCTChecks(typeof(Program));
}
static void Main(string[] args)
{
// Normal operation
Console.WriteLine("Foo");
Console.ReadLine();
}
}