Подтвердить что ты не робот

Почему я не могу указать значение по умолчанию в качестве необязательного параметра, кроме null?

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

private void Process(Foo f = new Foo())
{

}

Я получаю следующую ошибку (Foo - класс):

'f' - тип Foo, параметр по умолчанию ссылочного типа, отличный от строки, может быть инициализирован только нулевым значением.

Если я изменяю Foo на struct, тогда он работает, но с единственным конструктором без параметров.

Я прочитал документацию, и в ней четко сказано, что я не могу этого сделать, но в ней не упоминается почему?. Почему это ограничение существует и почему string исключается из этого? Почему значение необязательного параметра должно быть константой времени компиляции? Если бы это не было константой, то какими были бы побочные эффекты?

4b9b3361

Ответ 1

Исходной точкой является то, что CLR не поддерживает этого. Он должен быть реализован компилятором. Что-то, что вы можете увидеть из небольшой тестовой программы:

class Program {
    static void Main(string[] args) {
        Test();
        Test(42);
    }
    static void Test(int value = 42) {
    }
}

Что декомпилирует:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       15 (0xf)
  .maxstack  8
  IL_0000:  ldc.i4.s   42
  IL_0002:  call       void Program::Test(int32)
  IL_0007:  ldc.i4.s   42
  IL_0009:  call       void Program::Test(int32)
  IL_000e:  ret
} // end of method Program::Main

.method private hidebysig static void  Test([opt] int32 'value') cil managed
{
  .param [1] = int32(0x0000002A)
  // Code size       1 (0x1)
  .maxstack  8
  IL_0000:  ret
} // end of method Program::Test

Обратите внимание на то, что между двумя операторами вызова нет никакой разницы после завершения компилятора. Именно компилятор применил значение по умолчанию и сделал это на сайте вызова.

Также обратите внимание, что это все еще нужно работать, когда метод Test() фактически живет в другой сборке. Это означает, что значение по умолчанию должно быть закодировано в метаданных. Обратите внимание, как это сделала директива .param. Спецификация CLI (Ecma-335) документирует ее в разделе II.15.4.1.4

Эта директива хранит в метаданных a постоянное значение, связанное с номером параметра метода Int32, см. §II.22.9. Хотя CLI требует, чтобы для параметра было задано значение, некоторые инструменты могут использовать наличие этого атрибута, чтобы указать, что инструмент, а не пользователь, предназначен для предоставления значения параметр. В отличие от инструкций CIL,.param использует индекс 0 для указания возвращаемого значения метода, index 1, чтобы указать первый параметр метода, индекс 2, чтобы указать второй параметр метод и т.д.

[Примечание. CLI не связывает никаких семантических данных с этими значениями - это полностью зависит от компиляторов реализовать любую семантику, которую они хотят (например, так называемые значения аргументов по умолчанию). end note]

В приведенном разделе II.22.9 подробно рассматривается то, что означает постоянное значение. Наиболее важная часть:

Тип должен быть одним из следующих: ELEMENT_TYPE_BOOLEAN, ELEMENT_TYPE_CHAR, ELEMENT_TYPE_I1, ELEMENT_TYPE_U1, ELEMENT_TYPE_I2, ELEMENT_TYPE_U2, ELEMENT_TYPE_I4, ELEMENT_TYPE_U4, ELEMENT_TYPE_I8, ELEMENT_TYPE_U8, ELEMENT_TYPE_R4, ELEMENT_TYPE_R8 или ELEMENT_TYPE_STRING; или ELEMENT_TYPE_CLASS со значением нуля

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

Примечательно, что это просто не проблема, вы всегда можете реализовать произвольное значение по умолчанию для аргумента ссылочного типа. Например:

private void Process(Foo f = null)
{
    if (f == null) f = new Foo();

}

Это вполне разумно. И тип кода, который вы хотите в методе, вместо сайта вызова.

Ответ 2

Потому что нет другой константы времени компиляции, чем null. Для строк строковые литералы являются такими константами времени компиляции.

Я думаю, что некоторые дизайнерские решения, стоящие за ним, возможно, были:

  • Простота реализации
  • Устранение скрытого/непредвиденного поведения
  • Ясность договора метода, особенно. в сценариях кросс-сборки

Давайте подробнее рассмотрим эти три вопроса, чтобы получить представление о том, как справиться с проблемой:

1. Простота реализации

Если ограничено постоянными значениями, как компилятор, так и CLR-задания довольно просты. Константные значения могут быть легко сохранены в метаданных сборки, и компилятор может легко. Как это делается, было указано в Ответ Hans Passant.

Но что могли бы сделать CLR и компилятор для реализации непостоянных значений по умолчанию? Существует два варианта:

  • Сохраните сами выражения инициализации и скомпилируйте их там:

    // seen by the developer in the source code
    Process();
    
    // actually done by the compiler
    Process(new Foo());  
    
  • Сгенерируйте thunks:

    // seen by the developer in the source code
    Process();
    …
    void Process(Foo arg = new Foo())
    {
        … 
    }
    
    // actually done by the compiler
    Process_Thunk();
    …
    void Process_Thunk()
    {
        Process(new Foo());
    }
    void Process()
    {
        … 
    }
    

Оба решения вводят намного больше новых метаданных в сборки и требуют сложной обработки компилятором. Кроме того, хотя решение (2) можно рассматривать как скрытую технику (а также (1)), это имеет последствия в отношении воспринимаемого поведения. Разработчик ожидает, что аргументы будут оцениваться на сайте вызова, а не в другом месте. Это может вызвать дополнительные проблемы, которые необходимо решить (см. Часть, связанную с контрактом метода).

2. Устранение скрытого/неожиданного поведения

Выражение инициализации могло быть сколь угодно сложным. Отсюда простой вызов:

    Process();

будет разворачиваться в комплексный расчет, выполняемый на сайте вызова. Например:

    Process(new Foo(HorriblyComplexCalculation(SomeStaticVar) * Math.Power(GetCoefficient, 17)));

Это может быть довольно неожиданным с точки зрения читателя, который не проверяет декларацию "Process" полностью. Он загромождает код, делает его менее читаемым.

3. Ясность метода контракта, особенно. в сценариях кросс-сборки

Подпись метода вместе со значениями по умолчанию налагает контракт. Этот контракт живет в определенном контексте. Если выражение инициализации требовало привязки к некоторым другим сборкам, что бы требовало от вызывающего? Как насчет этого примера, когда метод "CalculateInput" относится к "Other.Assembly":

    void Process(Foo arg = new Foo(Other.Assembly.Namespace.CalculateInput()))

Здесь точка, где способ, которым это будет реализовано, играет решающую роль в мышлении, является ли это проблемой или примечанием. В разделе "Простота" я изложил методы реализации (1) и (2). Поэтому, если выбрано (1), для вызова вызывающего абонента требуется "Другая сборка". С другой стороны, если были выбраны (2), гораздо меньше необходимости - с точки зрения воплощения - для такого правила, потому что генерируемый компилятором Process_Thunk объявляется в том же месте, что и Process, и, следовательно, естественно имеет ссылку на Other.Aseembly. Тем не менее, здравомыслящий разработчик языков мог бы хотя бы навязать такое правило, поскольку возможны несколько реализаций одного и того же, а также для стабильности и ясности метода контракта.

Тем не менее, сценарии сборочной сборки будут налагать ссылки на сборку, которые явно не видны из простого исходного кода на сайте вызова. И это опять-таки проблема удобства и удобочитаемости.

Ответ 3

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

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

private Bar Process()
{
    return Process(new Foo());
}

private Bar Process(Foo f)
{
    //Whatever.
}

Ответ 4

Параметры по умолчанию управляют вызывающим абонентом таким образом, что вы указываете параметр по умолчанию, он изменит вашу подпись методов во время компиляции. Из-за этого вам нужно предоставить постоянное значение, которое в вашем случае "новый Foo()" не является.

Вот почему вам нужна константа.