Lâu nay tôi đã nghĩ về những hạn chế thời gian gần biên dịch, vì vậy đây là một cơ hội hoàn hảo để khởi chạy khái niệm này.
Ý tưởng cơ bản là nếu bạn không thể thực hiện kiểm tra thời gian biên dịch, bạn nên thực hiện nó vào thời điểm sớm nhất có thể, về cơ bản là thời điểm ứng dụng bắt đầu. Nếu tất cả các kiểm tra đều ổn, ứng dụng sẽ chạy; nếu kiểm tra thất bại, ứng dụng sẽ thất bại ngay lập tức.
Hành vi
Kết quả tốt nhất có thể là chương trình của chúng tôi không biên dịch nếu các ràng buộc không được đáp ứng. Thật không may, điều đó là không thể trong triển khai C # hiện tại.
Điều tốt nhất tiếp theo là chương trình gặp sự cố ngay khi nó bắt đầu.
Tùy chọn cuối cùng là chương trình sẽ sập thời điểm mã được nhấn. Đây là hành vi mặc định của .NET. Đối với tôi, điều này là hoàn toàn không thể chấp nhận.
Điều kiện tiên quyết
Chúng ta cần phải có một cơ chế ràng buộc, vì vậy, nếu thiếu bất cứ điều gì tốt hơn ... hãy sử dụng một thuộc tính. Thuộc tính sẽ có mặt trên một ràng buộc chung để kiểm tra xem nó có phù hợp với điều kiện của chúng ta không. Nếu không, chúng tôi sẽ đưa ra một lỗi xấu.
Điều này cho phép chúng tôi làm những việc như thế này trong mã của chúng tôi:
public class Clas<[IsInterface] T> where T : class
(Tôi đã giữ where T:class
ở đây, vì tôi luôn thích kiểm tra thời gian biên dịch hơn kiểm tra thời gian chạy)
Vì vậy, điều đó chỉ khiến chúng tôi gặp 1 vấn đề, đó là kiểm tra xem tất cả các loại mà chúng tôi sử dụng có khớp với ràng buộc không. Nó có thể khó như thế nào?
Chúng ta hãy phá vỡ nó
Các kiểu chung luôn ở trên một lớp (/ struct / interface) hoặc trên một phương thức.
Kích hoạt một ràng buộc đòi hỏi bạn phải thực hiện một trong những điều sau đây:
- Biên dịch thời gian, khi sử dụng một loại trong một loại (kế thừa, ràng buộc chung, thành viên lớp)
- Biên dịch thời gian, khi sử dụng một kiểu trong thân phương thức
- Thời gian chạy, khi sử dụng sự phản chiếu để xây dựng một cái gì đó dựa trên lớp cơ sở chung.
- Thời gian chạy, khi sử dụng sự phản chiếu để xây dựng một cái gì đó dựa trên RTTI.
Tại thời điểm này, tôi muốn nói rằng bạn nên luôn luôn tránh làm (4) trong bất kỳ chương trình IMO nào. Bất kể, những kiểm tra này sẽ không hỗ trợ nó, vì nó thực sự có nghĩa là giải quyết vấn đề tạm dừng.
Trường hợp 1: sử dụng một loại
Thí dụ:
public class TestClass : SomeClass<IMyInterface> { ... }
Ví dụ 2:
public class TestClass
{
SomeClass<IMyInterface> myMember; // or a property, method, etc.
}
Về cơ bản, việc này bao gồm quét tất cả các loại, kế thừa, thành viên, tham số, v.v., v.v ... Nếu một loại là loại chung và có một ràng buộc, chúng tôi kiểm tra ràng buộc; nếu đó là một mảng, chúng tôi kiểm tra loại phần tử.
Tại thời điểm này tôi phải thêm rằng điều này sẽ phá vỡ sự thật rằng mặc định .NET tải các loại 'lười biếng'. Bằng cách quét tất cả các loại, chúng tôi buộc .NET runtime tải tất cả chúng. Đối với hầu hết các chương trình, điều này không phải là một vấn đề; Tuy nhiên, nếu bạn sử dụng các trình khởi tạo tĩnh trong mã của mình, bạn có thể gặp phải sự cố với cách tiếp cận này ... Điều đó nói rằng, tôi sẽ không khuyên ai làm điều này bằng mọi cách (ngoại trừ những điều như thế này :-), vì vậy nó không nên đưa ra bạn rất nhiều vấn đề
Trường hợp 2: sử dụng một loại trong một phương thức
Thí dụ:
void Test() {
new SomeClass<ISomeInterface>();
}
Để kiểm tra điều này, chúng tôi chỉ có 1 tùy chọn: dịch ngược lớp, kiểm tra tất cả các mã thông báo thành viên được sử dụng và nếu một trong số chúng là loại chung - hãy kiểm tra các đối số.
Trường hợp 3: Phản xạ, xây dựng chung thời gian chạy
Thí dụ:
typeof(CtorTest<>).MakeGenericType(typeof(IMyInterface))
Tôi cho rằng về mặt lý thuyết có thể kiểm tra điều này bằng các thủ thuật tương tự như trường hợp (2), nhưng việc thực hiện nó khó hơn nhiều (bạn cần kiểm tra xem có MakeGenericType
được gọi trong một số đường dẫn mã không). Tôi sẽ không đi vào chi tiết ở đây ...
Trường hợp 4: Phản xạ, RTTI thời gian chạy
Thí dụ:
Type t = Type.GetType("CtorTest`1[IMyInterface]");
Đây là trường hợp xấu nhất và như tôi đã giải thích trước đây nói chung là một ý tưởng tồi IMHO. Dù bằng cách nào, không có cách thực tế nào để tìm ra điều này bằng cách sử dụng séc.
Kiểm tra lô
Tạo một chương trình kiểm tra trường hợp (1) và (2) sẽ dẫn đến kết quả như sau:
[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);
}
}
}
Sử dụng mã
Chà, đó là phần dễ dàng :-)
// 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();
}
}