В течение некоторого времени я думал об ограничениях во время компиляции, так что это прекрасная возможность запустить концепцию.
Основная идея заключается в том, что если вы не можете выполнить проверку времени компиляции, вы должны сделать это в кратчайшие сроки, то есть в основном с момента запуска приложения. Если все проверки в порядке, приложение будет запущено; если проверка не пройдена, приложение мгновенно завершится неудачей.
Поведение
Лучший возможный результат - наша программа не компилируется, если ограничения не выполнены. К сожалению, это невозможно в текущей реализации C #.
Следующим лучшим моментом является то, что программа вылетает в момент запуска.
Последний вариант заключается в том, что программа будет аварийно завершать работу при нажатии на код. Это поведение по умолчанию .NET. Для меня это совершенно неприемлемо.
Предварительно требования
Нам нужен механизм ограничений, поэтому из-за отсутствия чего-то лучшего ... давайте использовать атрибут. Атрибут будет присутствовать поверх общего ограничения, чтобы проверить, соответствует ли оно нашим условиям. Если этого не произойдет, мы выдаем ужасную ошибку.
Это позволяет нам делать такие вещи в нашем коде:
public class Clas<[IsInterface] T> where T : class
(Я сохранил where T:class
здесь, потому что я всегда предпочитаю проверки во время компиляции проверкам во время выполнения)
Таким образом, это оставляет только одну проблему, которая проверяет, соответствуют ли все типы, которые мы используем, ограничению. Как трудно это может быть?
Давайте разберемся
Универсальные типы всегда находятся в классе (/ struct / interface) или в методе.
Для запуска ограничения необходимо выполнить одно из следующих действий:
- Время компиляции при использовании типа в типе (наследование, общее ограничение, член класса)
- Время компиляции при использовании типа в теле метода
- Время выполнения, когда используется отражение для создания чего-либо на основе базового базового класса.
- Время выполнения, при использовании отражения для построения чего-либо на основе RTTI.
На этом этапе я хотел бы заявить, что вы всегда должны избегать выполнения (4) в любой программе IMO. Несмотря на это, эти проверки не поддержат его, поскольку это фактически означает решение проблемы остановки.
Случай 1: использование типа
Пример:
public class TestClass : SomeClass<IMyInterface> { ... }
Пример 2:
public class TestClass
{
SomeClass<IMyInterface> myMember; // or a property, method, etc.
}
В основном это включает в себя сканирование всех типов, наследования, членов, параметров и т. Д. И т. Д. И т. Д. Если тип является универсальным типом и имеет ограничение, мы проверяем ограничение; если это массив, мы проверяем тип элемента.
На данный момент я должен добавить, что это нарушит тот факт, что по умолчанию .NET загружает типы «ленивый». Сканируя все типы, мы заставляем среду .NET загружать их все. Для большинства программ это не должно быть проблемой; тем не менее, если вы используете статические инициализаторы в своем коде, вы можете столкнуться с проблемами при таком подходе ... Тем не менее, я бы никому не советовал делать это в любом случае (кроме таких вещей :-), поэтому он не должен давать У тебя много проблем.
Случай 2: использование типа в методе
Пример:
void Test() {
new SomeClass<ISomeInterface>();
}
Чтобы проверить это, у нас есть только один вариант: декомпилировать класс, проверить все используемые токены-члены и, если один из них является универсальным типом, проверить аргументы.
Случай 3: Отражение, универсальная конструкция во время выполнения
Пример:
typeof(CtorTest<>).MakeGenericType(typeof(IMyInterface))
Я предполагаю, что теоретически возможно проверить это с помощью тех же приемов, что и в случае (2), но его реализация намного сложнее (вам нужно проверить, MakeGenericType
вызывается ли в каком-то пути кода). Я не буду вдаваться в подробности здесь ...
Случай 4: Отражение, время выполнения RTTI
Пример:
Type t = Type.GetType("CtorTest`1[IMyInterface]");
Это наихудший вариант развития событий, и, как я уже объяснял, вообще плохая идея, ИМХО. В любом случае, нет практического способа выяснить это с помощью чеков.
Тестирование лота
Создание программы, которая проверяет варианты (1) и (2), приведет к чему-то вроде этого:
[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);
}
}
}
Используя код
Ну, это легкая часть :-)
// 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();
}
}