한동안 나는 컴파일 시간에 가까운 제약에 대해 생각하고 있었으므로 이것은 개념을 시작할 수있는 완벽한 기회입니다.
기본적인 아이디어는 체크 컴파일 시간을 수행 할 수없는 경우 가장 빠른 시점에 수행해야한다는 것입니다. 기본적으로 응용 프로그램이 시작되는 순간입니다. 모든 검사가 정상이면 응용 프로그램이 실행됩니다. 확인에 실패하면 응용 프로그램이 즉시 실패합니다.
행동
최상의 결과는 제약 조건이 충족되지 않으면 프로그램이 컴파일되지 않는다는 것입니다. 불행히도 현재 C # 구현에서는 불가능합니다.
다음으로 가장 좋은 점은 프로그램이 시작되는 순간 충돌한다는 것입니다.
마지막 옵션은 코드를 누르는 순간 프로그램이 중단되는 것입니다. 이것이 .NET의 기본 동작입니다. 나에게 이것은 완전히 받아 들일 수 없다.
전제 조건
제약 메커니즘이 필요하므로 더 나은 것이 없으면 속성을 사용합시다. 속성이 일반 제약 조건 위에 존재하여 조건과 일치하는지 확인합니다. 그렇지 않으면 추악한 오류가 발생합니다.
이를 통해 코드에서 다음과 같은 작업을 수행 할 수 있습니다.
public class Clas<[IsInterface] T> where T : class
( where T:class
런타임 검사보다 항상 컴파일 타임 검사를 선호하기 때문에 여기에 보관했습니다. )
따라서 우리에게 사용되는 모든 유형이 제약 조건과 일치하는지 확인하는 문제는 하나만 남습니다. 얼마나 힘들어요?
헤어지자
제네릭 형식은 항상 클래스 (/ struct / interface) 또는 메서드에 있습니다.
제약 조건을 트리거하려면 다음 중 하나를 수행해야합니다.
- 형식에서 형식을 사용할 때 컴파일 시간 (상속, 일반 제약 조건, 클래스 멤버)
- 메소드 본문에서 유형을 사용할 때 컴파일 타임
- 리플렉션을 사용하여 일반 기본 클래스를 기반으로 무언가를 구성 할 때의 런타임
- RTTI를 기반으로 무언가를 생성하기 위해 리플렉션을 사용할 때의 런타임.
이 시점에서 모든 프로그램 IMO에서 항상 (4)를 수행하지 말아야한다고 말하고 싶습니다. 어쨌든 이러한 검사는 중지 문제를 효과적으로 해결한다는 의미이므로 지원하지 않습니다.
사례 1 : 유형 사용
예:
public class TestClass : SomeClass<IMyInterface> { ... }
예 2 :
public class TestClass
{
SomeClass<IMyInterface> myMember; // or a property, method, etc.
}
기본적으로 여기에는 모든 유형, 상속, 멤버, 매개 변수 등을 스캔하는 것이 포함됩니다. 유형이 일반 유형이고 제약 조건이있는 경우 제약 조건을 확인합니다. 배열이면 요소 유형을 확인합니다.
이 시점에서 기본적으로 .NET이 'lazy'유형을로드한다는 사실을 깨뜨릴 것이라고 덧붙여 야합니다. 모든 유형을 스캔하여 .NET 런타임이 모든 유형을로드하도록합니다. 대부분의 프로그램에서 이것은 문제가되지 않습니다. 여전히 코드에서 정적 이니셜 라이저를 사용하면이 접근법에 문제가 발생할 수 있습니다 ... 즉, 어쨌든 누군가 에게이 작업을 수행하도록 조언하지는 않습니다 (이와 같은 것을 제외하고는 :-). 많은 문제가 있습니다.
사례 2 : 메소드에서 유형 사용
예:
void Test() {
new SomeClass<ISomeInterface>();
}
이것을 확인하기 위해 우리는 단 하나의 옵션이 있습니다 : 클래스를 디 컴파일하고, 사용되는 모든 멤버 토큰을 확인하고 그중 하나가 일반 유형인지 확인하십시오-인수를 확인하십시오.
사례 3 : 리플렉션, 런타임 일반 구성
예:
typeof(CtorTest<>).MakeGenericType(typeof(IMyInterface))
이론적으로 사례 (2)와 비슷한 트릭으로 이것을 확인할 수 있지만 구현이 훨씬 어렵습니다 ( MakeGenericType
일부 코드 경로에서 호출 되는지 확인해야 함). 여기서 자세히 설명하지 않겠습니다 ...
사례 4 : 리플렉션, 런타임 RTTI
예:
Type t = Type.GetType("CtorTest`1[IMyInterface]");
이것은 최악의 시나리오이며 일반적으로 나쁜 아이디어 IMHO 전에 설명했듯이. 어느 쪽이든, 수표를 사용하여 이것을 알아낼 수있는 실용적인 방법은 없습니다.
로트 테스트
사례 (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();
}
}