Ефективне використання відображення в C #

Я пишу бібліотеку в C#, яку я згодом буду використовувати для програми, і хочу, щоб бібліотека була настільки ж продуктивною, наскільки це можливо (тобто не надмірно ускладнюйте речі, щоб зробити його більш ефективним). Однак у мене є це питання про те, як найбільш ефективно використовувати віддзеркалення на класі/методи та ілюструвати питання, я спростив мій клас багато чого:

class MyClass
{
    private static Dictionary methods;

    public void Method1()
    {
       //Do something.
    }

    public void Method2()
    {
       //Do something else.
    }
}

Now, what I want is from within the class (a private method yet to be created), take a string containing the method-name, then fire the method, simple as that. The easiest way of doing this would be to just look at the name, get a method with that name, and execute it, but that forces me to use reflection a lot of times. This private method can potentially be called thousands or tens of thousands of times, and it need to be quick. So I came up with two possible solutions. As you can probably see I've added a static dictionary containing string->object (replacing object with actual type, just wrote object cause that works with both my examples). Then I'd add a static constructor that goes trough the class and adds all the methodinfos to the methods-dictionary. And then comes the question, on creation of a new instance of the class, should I create bound delegates to the methods and put those in a non-static private dict, or should I simply fire the methods using the MethodInfo in the methods-dictionary?

У середньому випадку використання буде створити 10 екземплярів даного класу та викликати 1000 + дзвінків до методу, який повинен запускати або метод 1, або метод 2 залежно від його аргументу (і ні, перемикач не є варіантом через розширюваність клас, як сказав, це була спрощена версія). Який би найефективніший спосіб досягти цього?

9
Добре, я сподівався принаймні отримати його роботу на 3.5, але я не знаю, чи я вже зробив це неможливо xD. WP7 також була надією в якийсь момент, але я думаю, що я відірвався, використовуючи System.Threading.Tasks (хоча я думаю, що можу опустити це, визначивши, чи повинен я.
додано Автор Alxandr, джерело
Ну, просто підключивши його до сервера, сервер надсилає команди у формі <ім'я> ... \ n , а ім'я визначає, яку функцію слід бути виконаним. Я хочу, щоб я міг просто додавати функції з відповідними іменами (вірніше, я створив атрибут, який дає мені назву методів, відмінних від методу, тому що назви команд можуть бути числовими), тому що список імен є looooong, і я не хочу робити перемикач-футляр.
додано Автор Alxandr, джерело
Крім того, підкласи повинні мати можливість додавати нові методи, які може викликати сервер.
додано Автор Alxandr, джерело
@JonAlb Я думаю, MEF буде способом перебору для цього простого завдання.
додано Автор Alxandr, джерело
Отже, у вас є свій атрибут. І ви маєте словник ключів і методів інформації. Я не можу побачити труднощі тут.
додано Автор Sam Axe, джерело
Просто цікаво, ви згадали про те, що розширюваність класу важлива, чому, на вашу думку, не здається, що створення правильного дизайну (без використання відображення) буде працювати? Може бути, якщо ви зможете пояснити свої вимоги до розширюваності, абстраговані від відображення, можна отримати краще рішення.
додано Автор Jordão, джерело
Це виглядає більше як завдання для dynamic , ніж для відображення ... Яка ваша версія Fx?
додано Автор Henk Holterman, джерело
Чи не буде MEF більш сучасним/ефективним/надійним способом підключити все разом? msdn.microsoft.com/en-us/library/dd460648.aspx
додано Автор JonAlb, джерело

8 Відповіді

Очевидно, що ніхто не може відповісти на питання, не намагаючись це робити, а робити тести на продуктивність, щоб дізнатися, чи досягнуто вашої мети.

Віддзеркалення набагато швидше в сучасних версіях системи, ніж раніше, але це все ще не так швидко, як просто виклик делегата.

Моя пропозиція полягає в тому, щоб почати з запропонованого вами рішення: будувати один раз кеш методу:

class MyClass
{
    static Dictionary cache = new ...
    public void InvokeByName(string name)
    {
        MethodInfo methodInfo = GetMethodInfoFromCache(name);
        methodInfo.Invoke(this, new object[] {});
    }

Коли з'явиться запит на виклик методу, ідентифікованого рядком на конкретному прикладі як приймач, знайдіть інформацію про метод за назвою, а потім зателефонуйте йому до даного приймача. Виміряйте продуктивність і подивіться, чи відповідає вона вашою цілі. Якщо це так, здорово; не витрачайте більше на свій дорогоцінний час, намагаючись зробити щось швидше, це вже досить швидко.

Якщо це не досить швидко, ось що я б робив:

class MyClass
{
    static Dictionary> cache = new ...
    public void InvokeByName(string name)
    {
        GetActionFromCache(name).Invoke(this);            
    }

Тож що робить GetActionFromCache? Якщо в кеш-пам'яті вже є дія, ми закінчили. Якщо цього немає, то отримаєте MethodInfo за допомогою Reflection. Потім скористайтеся бібліотекою дерева виразів, щоб створити Lambda:

var methodInfo = SomehowGetTheMethodInfo(name);
// We're going to build the lambda (MyType p)=>p.()    
var p = Expression.Parameter(typeof(MyType), "p"));
var call = Expression.Call(p, methodInfo);
var lambda = Expression.Lambda>(call, p);
var action = lambda.Compile();

І тепер у вас є дія в руці, яку ви можете викликати з екземпляром. Прикріпіть це в кеш-пам'яті.

Це, до речі, на неймовірно спрощеному рівні, як працює "динамічний" в C# 4. Наша проблема надзвичайно ускладнюється тим фактом, що ми повинні мати справу з приймачем і аргументами, що складають будь-який тип. У вас це дуже просто порівняно.

12
додано
Чи можу я просто запитати, в якій шкалі вона знаходиться? Це в тисячах або мільйонах? У всякому разі, я думаю, що я приймаю вашу пораду та просто дотримуюся методу MethodInfo зараз, і якщо це занадто повільно, я зроблю щось про це тоді :-).
додано Автор Alxandr, джерело
Це дуже здорова порада. Варто також згадати, що обчислення хешу імені методу і здійснення пошуку словника не є вільним, а компіляція лямбда є (порівняно) надзвичайно дорогою, тому якщо ви не робите багатьох викликів, то чисте відображення є хорошим шанс бути в цілому найшвидшим.
додано Автор Morten Mertner, джерело
Чорт ти Lippert ... Ви вкрали свою відповідь ... =)
додано Автор casperOne, джерело
@ EricLippert: Ви лещите мене, добросерф, дуже вдячний.
додано Автор casperOne, джерело
@casperOne: Великі уми думають однаково!
додано Автор Eric Lippert, джерело
Чому уникати дерева виразів, коли Delegate.CreateDelegate виконає цю роботу?
додано Автор Ani, джерело

Оскільки ви отримаєте всі MethodInfo , а також назву для їхнього розташування (імовірно, через MethodInfo.Name , ви можете перейти на один крок далі і створити компілюється лямбда-вираз у вигляді делегата, який ви можете виконати.

First, it's assumed that all of your methods will have the same signature. In this case, it's an Action delegate. With that, your dictionary will look like this:

// No need to have the dictionary **not** readonly
private static readonly IDictionary> methods =
    new Dictionary>;

Тоді, у вашому статичному конструкторі, ви б використовували відображення, щоб отримати всі приклади MethodInfo :

static MyClass()
{
   //Cycle through all the public instance methods.
   //Should filter down to make sure signatures match.
    foreach (MethodInfo methodInfo in typeof(MyClass).
        GetMethods(BindingFlags.Public | BindingFlags.Instance))
    {
       //Create the parameter expression.
        ParameterExpression parameter = Expression.
            Parameter(typeof(MyClass), "mc");

       //Call the method.
        MethodCallExpression body = Expression.Call(pe, methodInfo);

       //Compile into a lambda.
        Action action = Expression.Lambda>(
            body, parameter).Compile();

       //Add to the dictionary.
        methods.Add(methodInfo.Name, action);
    }
}

Тоді ваш приватний метод виглядатиме так:

private void ExecuteMethod(string method)
{
   //Add error handling.
    methods[method]();
}

Перевага тут полягає в тому, що ви отримуєте продуктивність складеного коду, плативши дуже низьку ціну (ІМО) у складності коду (при створенні делегата). Отримано невелику нагоду при виклику коду через делегата, але це значно покращилося (це повинно було бути з введенням LINQ, оскільки вони будуть виконуватися багато, багато разів).

4
додано
@Тузо: ярмарок досить =)
додано Автор casperOne, джерело
-1 для крадіжки відповіді Еріка Ліпперта ... жарту! ;)
додано Автор Randy Levy, джерело

Якщо б я мав вибір тут, я б, ймовірно, пішов із пропозицією Генка і використовував динаміку. Виклики методу є надзвичайно швидкими (набагато швидше, ніж звичайні відображення і майже як звичайні виклики методу).

Ви також можете отримати натхнення, переглянувши цей клас , який розширює DynamicObject і показує, як можна виконувати динамічне виклик методів.

Однак, якщо ви хочете підтримати версію 3.5 або залишити свої параметри відкритими, і у вас немає заперечень щодо використання сторонньої бібліотеки то це все одно можна зробити досить легко:

void Invoke( string methodName )
{
    this.CallMethod( methodName );
}

CallMethod створює делегат DynamicMethod для виклику цього конкретного методу і кешування його, якщо ви знову викликаєте той самий метод. Існують також розширення для виклику методів з параметрами та тоні інших допоміжних інструментів відбиття.

Якщо ви вважаєте за краще кешувати делегата самостійно (ми використовуємо ConcurrentDictionary та WeakReferences для цього, це означає, що він може зібрати сміття), просто зателефонувати за допомогою DelegateForCallMethod .

Бібліотека підтримує як 3.5, так і 4.0. WP7 не підтримує Reflection.Emit і тому не може використовувати генерацію IL і DynamicMethod. Тим не менш, WP 7.5 підтримує це (але Fasterflect, так як називається бібліотека, ще не підтримує).

2
додано
@svick Це було б правильним, якщо делегат був лише посиланням, але ви забудете, що він також містить всі створені ІЛ. Це займає набагато більше простору, ніж просто посилання делегата, тому на практиці n значно більше m. Це особливо вірно для деяких більш розвинутих делегатів, які ми створюємо, які мають більше ніж одну справу (наприклад, копіювати всі властивості з a до b).
додано Автор Morten Mertner, джерело
Для того, щоб закрити тут для будь-яких читачів, Fasterflect використовує DynamicMethod внутрішньо, а використання динаміки насправді є поганою ідеєю.
додано Автор Morten Mertner, джерело
@svick Чому ви не можете використовувати динамічний? Він поставляється з хорошим помічником TryInvokeMember, щоб полегшити ситуацію та як так можна легко адаптуватися до його потреб. Я не кажу, що він отримає багато іншого, виходячи з цього хоча.
додано Автор Morten Mertner, джерело
@svick Fasterflect - це бібліотека та кеш усіх делегатів, які вона створює. Використання WeakReference дозволяє видаляти кешовані дані, якщо це необхідно, що, здається, більш розумним, коли ви не можете дізнатись, скільки делегатів буде створено. Хоча динамічність не може бути використана безпосередньо, клас пов'язаного DynamicBuilder показує, як отримати від DynamicObject (тобто динамічний) та як виконувати динамічне виклик, якщо вказано ім'я методу.
додано Автор Morten Mertner, джерело
Але якщо я правильно вас зрозумів, то кешовані дані - це лише делегат. Якщо ви турбуєтеся про споживану ними пам'ять, тоді використання WeakReference не допоможе вам багато чого. Замість того, щоб завжди використовувати n байти, ви використовуєте n + m байти, де n можна звільнити, але m завжди є. І розміри n і m аналогічні.
додано Автор svick, джерело
додано Автор svick, джерело
Делегат не містить будь-якого сформованого ІЛ, він не генерує жодного ІЛ. Якщо ви використовуєте щось на зразок DynamicMethod , то вам буде правильно, але тут не так.
додано Автор svick, джерело
Чому ви хочете використовувати WeakReference тут? В словнику все ще займе деякий пробіл, і сам WeakReference є еталонним типом, тому займає деякий простір. Таким чином, використання не призведе до значної економії пам'яті, що може призвести до погіршення продуктивності, оскільки вам доведеться відтворити делегат.
додано Автор svick, джерело
І ви не можете використовувати динамічний , так чому б турбуватися за допомогою DynamicObject ? І з питання, здається, OP може створити словник делегатів, тому пов'язаний код також не допоможе йому це зробити.
додано Автор svick, джерело
Я думаю, що ви не можете використовувати dynamic безпосередньо, якщо ви не знаєте назви методів, які ви збираєтеся зателефонувати під час компіляції.
додано Автор svick, джерело

З вашого коментарю:

Well, simply put it's connected to a server, the server sends commands in the form of ... , and the name determines what functionality should be performed. I want to be able to just add functions with matching names (or rather, I've created an attribute that lets me name methods other than their method-name cause command-names might be numeric), cause the list of names is looooong, and I don't want to do a switch-case.

Ви можете вирішити цю проблему за допомогою простого командного інтерфейсу та фабрики, що керує таблицями, для відповідного екземпляра (або типу, якщо екземпляри команди не повторюються).

public interface ICommand {
  void Execute();
}

public class Processor {
  private static Dictionary commands;
  static Processor() {
   //create and populate the table
  }
  public void ExecuteCommand(string name) {
   //some validation...
    commands[name].Execute();
  }
}

Ніяких роздумів не було.

Щоб створити нову команду, просто створіть новий клас, який реалізує ICommand і додає відповідну рядок в таблицю в стековий конструктор Processor .

public class FooCommand : ICommand {
  public void Execute() {
   //foo away!
  }
}

...

public class Processor {
  static Processor() {
    ...
    commands["foo"] = new FooCommand();
    ...
  }
}

У цьому дизайні є багато переваг, крім виконання. Ваші команди ізольовані один від одного, зміни до однієї команди або створення нових команд не впливатимуть на інші команди. Вони краще перевіряються і легше підтримувати. Навіть процесор може бути закритий (у способі OCP ), якщо ви можете зберегти свою таблицю наприклад, конфігураційний файл або база даних.

Ви, звичайно, можете знайти альтернативні конструкції та способи передачі параметрів командам, але я сподіваюсь, що це дасть вам основну ідею.

2
додано

У цьому конкретному випадку ви можете оголосити свій словник трохи по-різному і отримати результат, який ви після цього:

class MyClass
{
    private static Dictionary> methods;

    public void Method1()
    {
       //Do something.
    }

    public void Method2()
    {
       //Do something else.
    }
    static MyClass(){
       methods = new Dictionary>();
       foreach(var method in typeof(MyClass).GetMethods(
               BindingFlags.Public | BindingFlags.Instance)
       )
        {
            methods.Add(
                method.Name,
                Delegate.CreateDelegate(typeof(Action),method) 
                  as Action);
        }
    }
}

Цей код має перевагу не використання генерації коду. Однак, якщо у вас є методи різних підписів, то буде потрібний інший підхід. Тут ми створюємо делегованих відкритих екземплярів. (Зауважте, що це не завжди правильно, якщо MyClass є структурою або якщо будь-який з цих методів є загальними віртуальними методами).

1
додано

Invoking a MethodInfo is slow. So I think creating a new dictionary for each instance should be good enough. Another option is to create a delegate that accepts the instance (Action) using Expressions (and then store them in the static dictionary):

MethodInfo method = typeof(MyClass).GetMethod("Method1");

var parameter = Expression.Parameter(typeof(MyClass));

var call = Expression.Call(parameter, method);

var lambda = Expression.Lambda>(call, parameter);

Action del = lambda.Compile();
0
додано

Найшвидший спосіб зробити щось не робити це взагалі. Чи знаєте ви, що просто створюєте інтерфейс, як:

interface ICallMe 
{
 void CallByName(string name, object args);
}

Таким чином, якщо деякі з реалізацій хоче бути надзвичайно розумними, це може зробити рефлексію + кешування + створення IL, інші можуть просто використовувати if/switch.

Недоліком - значно менше задоволення для реалізації та налагодження.

0
додано
Проблема полягає в тому, що він повинен вручну ввести цей інтерфейс для кожного методу. Якщо у нього є безліч методів, які будуть нудна.
додано Автор Michael B, джерело

Ви розглядали можливість використання Кодування покоління та текстових шаблонів T4 ?

http://msdn.microsoft.com/en-us/library/bb126445.aspx
http://msdn.microsoft.com/en-us/library/bb126478.aspx

Тоді ви могли б скористатися твердженням справи.

Щось на зразок

partial Class MyClass
{
    public void Exec(string funcToExec)
    {
        swtich(funcToExec)
        {
            <#
            foreach(MethodInfo mi in 
                typeof(MyClass).GetMethods(BindingFlags.Public | BindingFlags.Static)
            { 
                if(mi.Name != "Exec"){
            #>
            case : "<#= mi.Name #>"
                <#= mi.Name #>();
            <#
            }}
            #>
        }
    }
}
0
додано
var chat = new Chat();
var chat = new Chat();
642 учасників

Обсуждение вопросов по C# / .NET / .NET Core / .NET Standard / Azure Сообщества-организаторы: — @itkpi — @dncuug