Почему компилятор 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();

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

15.12.2008 05:56:35
Я заметил то же самое ... но я не знаю почему.
Chuck Conway 15.12.2008 06:27:07
Я использую сниппет компилятор и компилятор не выдает никаких ошибок. Кроме того, конструктор вызывается, когда вызывается new T ().
shahkalpesh 15.12.2008 06:42:15
@shahkalpesh: никто не сказал, что будет ошибка. Дело в том, что Activator.CreateInstance работает медленнее, чем форма делегата.
Jon Skeet 15.12.2008 06:56:09
@Jon: Это на уровне IL, вставлен вызов Activator.CreateInstance? Если так, то я не понял этого вопроса.
shahkalpesh 15.12.2008 07:07:11
@shahkalpesh: Да. Запустите Reflector или ildasm над кодом, используя новый T () (с новым ограничением T (), а не структурным), и вы увидите это.
Jon Skeet 15.12.2008 08:18:47
5 ОТВЕТОВ

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

Просто предположение, но это имеет определенное количество смысла.

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

9
30.06.2016 22:24:28

Это вероятно потому, что неясно, является ли 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.

8
15.12.2008 07:04:51
Я предполагаю, что непосредственным последующим вопросом будет «почему нет соответствующей инструкции IL для создания экземпляра универсального типа с соответствующим ограничением?» Не то чтобы они не могли встроить это с самого начала :)
Jon Skeet 15.12.2008 08:18:05
Согласитесь, действительно кажется, что они реализовали API вместо инструкции IL. В комментарии на странице документации MSDN для Activator.CreateInstance конкретно сказано, что его следует вызывать для этого сценария. Странный выбор, я уверен, что есть веская причина.
JaredPar 15.12.2008 08:27:27
Я подозреваю, что причина в том, чтобы увеличить совместное использование кода JIT. Если у вас был прямой вызов конструктора типа в коде JIT, то вы не могли бы поделиться этим кодом JIT с другим экземпляром для другого типа, например, 'T Create & lt; T & gt; () где T: new ( ) {return new T ();} 'предоставит общий машинный код для Create & lt; string & gt; () и Create & lt; ArrayList & gt; ().
jonp 10.06.2009 19:24:36
@JonSkeet Оглядываясь назад на это через пять лет, кажется, что это растущая тенденция: использование статических методов для обозначения мест, где JIT должен взять верх, в отличие от создания новых инструкций. Хорошим примером будет CER.
Zenexer 15.06.2013 11:02:26
Просто быстрое замечание, что это больше не так, к сожалению. Независимо от ограничений, Roslyn выходит Activator.CreateInstance.
nawfal 30.06.2016 10:38:21

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

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

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

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

2
15.12.2008 07:17:14
Я не думаю, что это сработает, потому что это именно «новый T ()», которого его обходной путь пытается избежать.
Joel Mueller 10.06.2009 19:22:39
@ Джоэл Мюллер На самом деле это работает. Здесь дерево выражений содержит выражение NewExpression.
ghord 6.06.2013 15:25:26
Да, это выражение Func <T>, а не Func <T>. "() => New T ()" не производит IL (таким образом производит Activator.CreateInstance ()), но дерево выражений, которое в свою очередь компилируется во время выполнения, когда T известен. Единственная проблема здесь в том, что каждый раз, когда вы вызываете эту функцию, вы перекомпилируете этот оператор.
Thanasis Ioannidis 5.12.2013 13:33:11
Это замечательно, не знал, что это может сработать. Для неинформированных, скомпилированный IL будет иметь инструкции, Expression.Newа не Activator.CreateInstance. Чувствую себя обманщиком, хотя .. довольно неинтуитивно и менее очевидно для меня.
nawfal 1.07.2016 01:03:24

Почему этот обходной путь необходим?

Потому что новое ограничение () было добавлено в C # 2.0 в .NET 2.0.

Тем временем выражение <T> и друзья были добавлены в .NET 3.5.

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

3
10.06.2009 19:12:26

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

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; } }
2
1.07.2016 00:58:26