บางครั้งตอนนี้ฉันกำลังคิดถึงข้อ จำกัด ที่ใกล้เคียงกันดังนั้นนี่เป็นโอกาสที่สมบูรณ์แบบสำหรับการเปิดตัวแนวคิด
แนวคิดพื้นฐานคือถ้าคุณไม่สามารถรวบรวมเวลาตรวจสอบได้คุณควรดำเนินการให้เร็วที่สุดเท่าที่จะเป็นไปได้ซึ่งเป็นช่วงเวลาที่แอปพลิเคชันเริ่มต้น หากการตรวจสอบทั้งหมดไม่เป็นไรแอปพลิเคชันจะทำงาน หากการตรวจสอบล้มเหลวแอปพลิเคชันจะล้มเหลวทันที
พฤติกรรม
ผลลัพธ์ที่ดีที่สุดที่เป็นไปได้คือโปรแกรมของเราไม่ได้รวบรวมหากข้อ จำกัด ไม่เป็นไปตาม น่าเสียดายที่ไม่สามารถใช้ C # ในปัจจุบันได้
สิ่งที่ดีที่สุดถัดไปคือโปรแกรมหยุดทำงานทันทีที่เริ่มทำงาน
ตัวเลือกสุดท้ายคือโปรแกรมจะหยุดทำงานทันทีที่มีการกดรหัส นี่คือพฤติกรรมเริ่มต้นของ. NET สำหรับฉันมันไม่สามารถยอมรับได้อย่างสมบูรณ์
Prerequirements
เราจำเป็นต้องมีกลไกที่มีข้อ จำกัด ดังนั้นหากขาดสิ่งใดที่ดีกว่า ... มาใช้แอตทริบิวต์กันเถอะ แอตทริบิวต์นี้จะปรากฏบนข้อ จำกัด ทั่วไปเพื่อตรวจสอบว่าตรงกับเงื่อนไขของเราหรือไม่ หากไม่เป็นเช่นนั้นเราจะให้ข้อผิดพลาดที่น่าเกลียด
สิ่งนี้ทำให้เราสามารถทำสิ่งนี้ในรหัสของเรา:
public class Clas<[IsInterface] T> where T : class
(ฉันเก็บไว้ที่where T:class
นี่เพราะฉันชอบตรวจสอบคอมไพล์เวลามากกว่าเช็ครันไทม์)
ดังนั้นนั่นทำให้เรามีเพียง 1 ปัญหาเท่านั้นซึ่งกำลังตรวจสอบว่าประเภททั้งหมดที่เราใช้ตรงกับข้อ จำกัด หรือไม่ มันยากเหลือเกิน
มาทำลายมันกันเถอะ
ประเภททั่วไปมักจะอยู่ในคลาส (/ 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 ในการโหลดทั้งหมด สำหรับโปรแกรมส่วนใหญ่สิ่งนี้ไม่ควรมีปัญหา ยังถ้าคุณใช้ initializers คงที่ในรหัสของคุณคุณอาจพบปัญหากับวิธีการนี้ ... ที่กล่าวว่าฉันจะไม่แนะนำให้ใครทำเช่นนี้ (ยกเว้นสิ่งเช่นนี้ :-) ดังนั้นจึงไม่ควรให้ คุณมีปัญหามากมาย
กรณีที่ 2: ใช้ชนิดในเมธอด
ตัวอย่าง:
void Test() {
new SomeClass<ISomeInterface>();
}
ในการตรวจสอบสิ่งนี้เรามีเพียง 1 ตัวเลือก: ถอดรหัสคลาสให้ตรวจสอบโทเค็นสมาชิกทั้งหมดที่ใช้และถ้าหนึ่งในนั้นคือประเภททั่วไป - ตรวจสอบข้อโต้แย้ง
กรณีที่ 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();
}
}