Почему компилятор c # выдает Activator.CreateInstance при вызове new с универсальным типом с ограничением new ()?

Когда у вас есть такой код:

static T GenericConstruct<T>() where T : new()
{
    return new T();
}

Компилятор C # настаивает на вызове Activator.CreateInstance, что значительно медленнее, чем собственный конструктор.

У меня есть следующий обходной путь:

public static class ParameterlessConstructor<T>
    where T : new()
{
    public static T Create()
    {
        return _func();
    }

    private static Func<T> CreateFunc()
    {
        return Expression.Lambda<Func<T>>( Expression.New( typeof( T ) ) ).Compile();
    }

    private static Func<T> _func = CreateFunc();
}

// Example:
// Foo foo = ParameterlessConstructor<Foo>.Create();

Но для меня не имеет смысла, зачем нужен этот обходной путь.


person user46267    schedule 15.12.2008    source источник
comment
Я заметил то же самое ... но не знаю почему.   -  person Chuck Conway    schedule 15.12.2008
comment
Я использую компилятор фрагментов кода, и компилятор не выдает ошибок. Также конструктор вызывается при вызове new T ().   -  person shahkalpeshp    schedule 15.12.2008
comment
@shahkalpesh: Никто не говорил, что будет ошибка. Дело в том, что Activator.CreateInstance работает медленнее, чем форма делегата.   -  person Jon Skeet    schedule 15.12.2008
comment
@Jon: На уровне IL вставлен вызов Activator.CreateInstance? Если так, то я не понял из вопроса.   -  person shahkalpeshp    schedule 15.12.2008
comment
@shahkalpesh: Да. Запустите Reflector или ildasm поверх кода, используя new T () (с новым ограничением T (), а не структурным ограничением), и вы увидите это.   -  person Jon Skeet    schedule 15.12.2008
comment
Кстати, все компиляторы VB.NET, которые я могу протестировать, всегда производят вызов Activator::CreateInstance для общих, class и structure ограничений.   -  person Mark Hurd    schedule 16.05.2014


Ответы (5)


Я подозреваю, что это проблема JITting. В настоящее время JIT повторно использует один и тот же сгенерированный код для всех аргументов ссылочного типа, поэтому vtable List<string> указывает на тот же машинный код, что и List<Stream>. Это не сработало бы, если бы каждый new T() вызов нужно было разрешать в JITted-коде.

Только предположение, но в этом есть определенный смысл.

Один интересный момент: в ни в том, ни в другом случае не вызывается конструктор без параметров для типа значения, если он есть (что исчезающе редко). См. мое недавнее сообщение в блоге для подробностей. Я не знаю, есть ли способ принудительно использовать это в деревьях выражений.

person Jon Skeet    schedule 15.12.2008

Вероятно, это связано с тем, что неясно, является ли T типом значения или ссылочным типом. Создание этих двух типов в неуниверсальном сценарии приводит к очень разному IL. Перед лицом этой неоднозначности C # вынужден использовать универсальный метод создания типов. Activator.CreateInstance отвечает всем требованиям.

Быстрые эксперименты, кажется, подтверждают эту идею. Если вы введете следующий код и изучите IL, он будет использовать initobj вместо CreateInstance, потому что в типе нет двусмысленности.

static void Create<T>()
    where T : struct
{
    var x = new T();
    Console.WriteLine(x.ToString());
}

Переключение на класс и ограничение new () по-прежнему вызывает Activator.CreateInstance.

person JaredPar    schedule 15.12.2008
comment
Я предполагаю, что сразу же возникнет следующий вопрос: почему нет соответствующей инструкции IL для создания экземпляра универсального типа с соответствующим ограничением? Не то чтобы они не могли встроить это с самого начала :) - person Jon Skeet; 15.12.2008
comment
Согласны, действительно похоже, что они реализовали API вместо инструкции IL. В комментарии на странице документации MSDN для Activator.CreateInstance конкретно говорится, что его следует вызывать для этого сценария. Странный выбор, я уверен, что на это есть веская причина. - person JaredPar; 15.12.2008
comment
Я подозреваю, что причина в том, чтобы увеличить совместное использование кода JIT. Если бы у вас был прямой вызов конструктора типа в JIT-коде, вы не могли бы поделиться этим JIT-кодом с другим экземпляром для другого типа, например 'T CreateT (), где T: new () {return new T ();}' будет совместно использовать машинный код для Createstring () и CreateArrayList (). - person jonp; 10.06.2009
comment
@JonSkeet Оглядываясь на это пятью годами позже, кажется, что это растущая тенденция: использование статических методов для обозначения мест, где JIT должен взять верх, в отличие от создания новых инструкций. Хорошим примером может служить CER. - person Zenexer; 15.06.2013
comment
Замечу, что, к сожалению, это уже не так. Независимо от ограничений, Roslyn выводит Activator.CreateInstance. - person nawfal; 30.06.2016

Зачем нужен этот обходной путь?

Поскольку общее ограничение new () было добавлено в C # 2.0 в .NET 2.0.

Тем временем Expression ‹T› и его друзья были добавлены в .NET 3.5.

Итак, ваш обходной путь необходим, потому что это было невозможно в .NET 2.0. Между тем, (1) использование Activator.CreateInstance () было возможно, и (2) в IL отсутствует способ реализации 'new T ()', поэтому Activator.CreateInstance () использовался для реализации этого поведения.

person jonp    schedule 10.06.2009

Интересное наблюдение :)

Вот более простой вариант вашего решения:

static T Create<T>() where T : new()
{
  Expression<Func<T>> e = () => new T();
  return e.Compile()();
}

Очевидно наивно (и возможно медленно) :)

person leppie    schedule 15.12.2008
comment
Я не думаю, что это сработает, потому что его обходной путь пытается избежать именно нового T (). - person Joel Mueller; 10.06.2009
comment
@Joel Mueller На самом деле это работает. Здесь дерево выражений содержит NewExpression. - person ghord; 06.06.2013
comment
Да, это выражение Func ‹T›, а не Func ‹T›. () = ›New T () не создает IL (таким образом, создает Activator.CreateInstance ()), а создает дерево выражений, которое, в свою очередь, компилируется во время выполнения, когда T известен. Единственная проблема здесь в том, что каждый раз, когда вы вызываете эту функцию, вы перекомпилируете этот оператор. - person Thanasis Ioannidis; 05.12.2013
comment
Это великолепно, не знал, что это может сработать. Для неинформированных, скомпилированный IL будет содержать инструкции для Expression.New, а не Activator.CreateInstance. Похоже на жульничество… для меня это совершенно не интуитивно и менее очевидно. - person nawfal; 01.07.2016

Это немного быстрее, поскольку выражение компилируется только один раз:

public class Foo<T> where T : new()
{
    static Expression<Func<T>> x = () => new T();
    static Func<T> f = x.Compile();

    public static T build()
    {
        return f();
    }
}

Анализируя производительность, этот метод так же быстр, как и более подробное скомпилированное выражение, и намного, намного быстрее, чем new T() (в 160 раз быстрее на моем тестовом ПК).

Для чуть более высокой производительности вызов метода сборки может быть исключен, а вместо него может быть возвращен функтор, который клиент может кэшировать и вызывать напрямую.

public static Func<T> BuildFn { get { return f; } }
person Community    schedule 15.08.2009