Ну что ж, исправим это!
Узнаем причины отсутствия наследования у структур, природу unbound delegates.
А еще… вызов любых методов у любых объектов без reflection.
▌Genesis of Value-types
Структуры в .NET являются с одной стороны структурами в классическом понимании данного слова (layout, mutability и т.д.), с другой стороны имеют поддержку ООП и среды .NET в принципе (методы ToString, GetHashCode; наследование от System.ValueType, который в свою очередь от System.Object; и т.д.).
Чтобы лучше понять почему структуры нельзя наследовать от других типов, необходимо перейти на уровень организации методов в CLR.
Instance-level методы имеют неявный аргумент this. На самом деле он явный. JIT, компилируя код, создает сигнатуру следующего вида:
ReturnType MethodName(Type this, …arguments…)
Но это для ссылочных типов.
Для значимых:
ReturnType MethodName(ref Type this, …arguments…)
Да-да! Сделано это для поддержки изменяемости структур, т.е. чтобы мы могли модифицировать this.
Так почему же нельзя наследовать структуры от других типов?
Ответим на вопрос: а если это будет виртуальный метод базового ссылочного класса? Как быть JIT-компилятору? Никак. Постоянно угадывать и генерировать различные специализации кода (с семантикой byval и byref), кроме еще и диспетчеризации таблицы виртуальных методов – неэффективно. Добавляется и boxing, чтобы правильно обслужить виртуальный метод.
Но… Методы ToString, GetHashCode, Equals являются виртуальными методами ссылочного класса-предка System.Object ?!
Это исключения. JIT знает об этом и генерирует привязку и специализацию только для этих методов.
▌Unbound Delegates
Reflection в .NET позволяет нам создать делегат как на статические методы, так и на экземпляров.
Однако есть небольшая проблема – для экземпляров необходимо создавать делегату по новому.
class Program { static void Main(string[] args) { var calc = new Calc() { FirstOperand = 2 }; var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (Func<int, int>)Delegate.CreateDelegate( typeof(Func<int, int>), calc, addMethodInfo); Console.WriteLine(addDelegate(2)); // 4 } } class Calc { public int FirstOperand = 0; public int Add(int secondOperand) { return FirstOperand + secondOperand; } }
На помощь приходят unbound delegates, т.е. непривязанные. Однако у них есть одна особенность: иная сигнатура, где добавляется (да, Вы правильно догадались) первый аргумент – ссылка на экземпляр.
Т.е. unbound delegates – это и есть ссылки на “реальный” метод.
Так, сигнатура Add(int secondOperand) превратиться в Add(Calc this, int secondOperand).
class Program { static void Main(string[] args) { var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (Func<Calc, int, int>)Delegate.CreateDelegate( typeof(Func<Calc, int, int>), null, addMethodInfo); Console.WriteLine(addDelegate(new Calc(), 2)); // 2 } } class Calc { public int FirstOperand = 0; public int Add(int secondOperand) { return FirstOperand + secondOperand; } }
Помните вопрос про сигнатуры методов структур? Объявите тип Calc как struct и запустите. ArgumentException? Да?
Нам нужно передать в Func<Calc,int,int> аргумент this byref, но как?!
delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2);
class Program { delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2); static void Main(string[] args) { var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (FuncByRef<Calc, int, int>)Delegate.CreateDelegate( typeof(FuncByRef<Calc, int, int>), null, addMethodInfo); var calc = new Calc(); calc.FirstOperand = 123; Console.WriteLine(addDelegate(ref calc, 2)); // 125 } } struct Calc { public int FirstOperand; public int Add(int secondOperand) { return FirstOperand + secondOperand; } }
▌Unbound Delegates
Рассмотрим простое приложение:
class Program { static void Main(string[] args) { CallTest(new object()); CallTestWithExlicitCasting(new object()); Console.Read(); } static void CallTest(object target) { Program p = target as Program; p.Test(); } static void CallTestWithExlicitCasting(object target) { Program p = (Program)target; p.Test(); } public void Test() { Console.WriteLine("Test"); } }
Как можно заметить, приложение упадет с NullReferenceException при вызове CallTest().
Что ж, исправим данную ситуацию. Для этого запустим ildasm.
Visual Studio Command Promt -> ildasm

Далее File -> Dump -> Save as dialog -> msiltricks_patch.il
Открываем сохраненный файл msiltricks_patch.il в любимом редакторе и на ходим тело метода CallTest:
.method private hidebysig static void CallTest(object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: isinst MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program::Test() IL_000d: ret } // end of method Program::CallTest
Удалим сроку IL_0001: isinst MSILTricks.Program, т.е. вызов оп-кода isinst (он же оператор as в C#).
Проделываем то же самое и с методом CallTestWithExlicitCasting:
.method private hidebysig static void CallTestWithExlicitCasting(object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: castclass MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program::Test() IL_000d: ret } // end of method Program::CallTestWithExlicitCasting
Удалим сроку IL_0001: castclass MSILTricks.Program, т.е. вызов оп-кода castclass (он же оператор явного приведения в C#).
Visual Studio Command Promt -> cd [your saved file dir]
Visual Studio Command Promt -> ilasm msiltricks_patch.il
Запустим msiltricks_patch.exe
Ни одного исключения, даже AccessViolationException.
Ха-ха!
Дело в том, что наш метод Test не имеет побочных эффектов, а также не использует this в своем теле.
Вывод: мы с Вами работаем с “железом” и переменные ссылочных типов являются просто адресами в памяти, т.е. DWORD; приведение типов и т.д. являются не более чем абстракцией и “защитой” на этапе компиляции. Центральный процессор работает именно с адресами в памяти. CLR предоставляет эти адреса, JIT компилирует код, учитывая их.
Ваш КО 🙂
И, да, инструкция callvirt не проверяет на “правильность” объекта.
Чтобы получить AccessViolationException, можно добавить, например, виртуальный метод в класс Program и вызвать его в методе Test.
ссылка на оригинал статьи http://habrahabr.ru/post/248775/
Добавить комментарий