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

Если у вас есть код вроде следующего:

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

Компилятор С# настаивает на том, чтобы вызывать вызов 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();

Но мне не имеет смысла, почему это обходное решение необходимо.

Ответы

Ответ 1

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

Просто догадайтесь, но это имеет определенный смысл.

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

Ответ 2

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

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

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

Переключение на ограничение класса и new(), хотя все равно вынуждает Activator.CreateInstance.

Ответ 3

Почему это обходное решение необходимо?

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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; } }