Автор: Sergey Teplyakov
При наличии общего механизма перегрузки функций аргументы по умолчанию становятся логически избыточными и в лучшем случае обеспечивают небольшое удобство нотации Б. Страуструп Дизайн и эволюция С++
Многие разработчики, перешедшие в .NET с языка С++ хотели увидеть в C# аргументы по умолчанию. И вот в C# 4.0 они появились, но с существенными ограничениями, особенно по сравнению с языком С++. Конечно эти особенности и ограничения не сложно запомнить, а их перечисление займет не больше пары строк, но вместо простого перечисления этих особенностей я предлагаю побывать в шкуре разработчиков этой фичи для языка платформы .NET и прийти к этим ограничениям и особенностям самостоятельно.
Варианты реализации
Смысл аргументов по умолчанию очень простой: класс, предоставляющий метод знает значение аргумента, подходящее многим его клиентам по умолчанию. Поэтому вместо перегрузки методов он использует один метод и делает один или несколько аргументов не обязательными:class StreamWrapper{public static void UseStream(Stream stream, bool needClose = true) { }}
Прежде чем рассматривать реализацию аргументов по умолчанию в языке C# давайте рассмотрим варианты реализации этой возможности в принципе.
1. Использование перегрузки методов
Так, например, при наличии аргументов по умолчанию, компилятор мог бы создать два метода, аналогично тому, как мы поступаем в языках без аргументов по умолчанию:// UseStream(Stream stream, bool needClose = true) преоразуется в 2 метода:public static void UseStream(Stream stream, bool needClose){ }public static void UseStream(Stream stream){ UseStream(stream, true),}
Это решение на первый взгляд может показаться разумным, но оно приведет к ряду проблем:
1. Как быть со стек-трейсом? Если при вызове второго метода произойдет исключение, то стек-трейс будет содержать детали реализации этой возможности. Можно заставить компилятор инлайнить второй вызов и просто продублировать код из оригинального метода, но это не всегда просто и возможно.
2. Как быть с языками, не знающими об аргументах по умолчанию? Для них среда разработки будет показывать два метода, что совсем не удобно!
3. Как быть с виртуальными методами и интерфейсами? Опять-таки, в языках, не знающих об аргументах по умолчанию появится возможность переопределить или реализовать лишь один метод.
Другими словами, чтобы реализовать аргументы по умолчанию таким способом потребуются изменения среды исполнения и других языков программирования. Поскольку аргументы по умолчанию это скорее хотелка, то такие ограничения сделают эту возможность экономически не выгодной с точки зрения ее разработки (и я не знаю ни одного примера, где аргументы по умолчанию были бы реализованы таким образом).
2. Подстановка значений по умолчанию в вызывающем коде
Альтернативным решением, будет использование одного метода и в случае вызова метода без одного из параметров, компилятор подставит втихаря значения по умолчанию:StreamWrapper.UseStream(stream),// Эта строка заменяется компилятором в *месте вызова* на следующую:// bool defVal = GetDefaultValueFromMetadata(),// StreamWrapper.UseStream(ms, defVal),
Именно таким образом реализованы аргументы по умолчанию в VB и С++, и именно так они и реализованы в C#. И хотя идея реализации этой возможности в языках С++ и C# похожи, разница тоже есть. Для подстановки значения по умолчанию в месте вызова компилятору нужно добраться до значения по умолчанию. И если для С++ это значит использование заголовочного файла, то в случае платформы .NET мы должны сохранить значение по умолчанию в метаданных сборки.
Реализация аргументов по умолчанию в C#
Итак, в языке C# используется подстановка значения по умолчанию в месте вызова метода. Как бы мы с вами реализовали эту возможность? Во-первых, при компиляции метода с аргументами по умолчанию, это значение мы бы сохранили в метаданных с помощью атрибутов (где же его еще хранить?).
В результате метод UseStream:class StreamWrapper{public static void UseStream(Stream stream, bool needClose = true) { }}
Во время компиляции преобразовали бы его во что-то такое:public static void UseStream(Stream stream, [Optional][DefaultParameterValue(value: true)]bool needClose){ }
После чего, изменили бы overload resolution таким образом, чтобы приведенный выше метод был доступен в случае вызова UseStream(ms). Затем в месте вызова осталось бы достать значение атрибута DefaultParameterValue и подставить его значение Value в качестве аргумента needClose.
ПРИМЕЧАНИЕАргументы по умолчанию интегрированы в платформу .NET несколько сильнее, чем я описал. Так, в метаданных параметров метода отсутствует DefaultParameterValueAttribute, вместо этого, значение по умолчанию будет храниться непосредственно в экземпляре ParameterInfo.
Ограничения аргументов по умолчанию
Поскольку теперь мы знаем, что аргументы по умолчанию в C# реализованы на основе атрибутов и подстановки значения по умолчанию в месте вызова, то не сложно догадаться об ограничениях и особенностях этой возможности.
1. Ограничение значений по умолчанию
Поскольку значение по умолчанию хранится в атрибутах, то мы не можем использовать в качестве аргумента по умолчанию произвольные выражения:void WithDateTime(DateTime dt = DateTime.Now) { }// Будет компилироваться в случае: string s = ''void WithStringEmpty(string s = String.Empty) { }void WithMethodCall(int id = GetInvalidId()) { }static int GetInvalidId() { return -1, }
Пользовательские атрибуты на данный момент могут хранить лишь константы времени компиляции, а значит аргументы по умолчанию также ограничены лишь константами. Можно было бы предложить компилятору сериализовывать выражения и сохранять его в атрибутах, но это решение потребовало бы на порядок больше усилий.
2. Проблема версионирования
Встраивание значение по умолчанию в место вызова происходит во время компиляции, а не во время исполнения. А значит мы получаем проблему версионирования, аналогичную той, что мы имеем с константами. При изменении значения по умолчанию метода, нам нужно перекомпилировать всех его клиентов, поскольку в противном случае, мы будем вызывать метод со старым значением по умолчанию.
3. Переопределение (overriding) значений по умолчанию
Поиск значения по умолчанию происходит во время компиляции, а это значит, что смешивать аргументы по умолчанию и полиморфизм нельзя. Давайте рассмотрим следующий код:public class StreamWrapper{public virtual void UseStream(Stream stream, bool needClose = true) {Console.WriteLine('NeedClose: {0}', needClose), }}public class AdvancedStreamWrapper : StreamWrapper{public override void UseStream(Stream stream, bool needClose = false) {base.UseStream(stream, needClose), }}var ms = new MemoryStream(),var streamWrapper = new AdvancedStreamWrapper(),streamWrapper.UseStream(ms),((StreamWrapper)streamWrapper).UseStream(ms),
В этом случае значение аргумента по умолчанию будет зависеть от статического типа переменной, через которую мы вызываем метод UseStream, поэтому в первом случае мы получим needClose равный False, а во втором случае needClose равный True.
Это довольно известная проблема в языке С++, и особенности реализации аргументов по умолчанию привнесли ее и в C#. При этом, тот же РеШарпер сразу же предупреждает об Redundant method override (хотя я бы посоветовал сменить предупреждение на ошибку, поскольку речь идет не просто об избыточности, а об изменении поведения!).
3. Использование специального типа Optional<,T>,
На самом деле, существует еще как минимум один распространенный способ реализации аргументов по умолчанию. Вместо подстановки значения в месте вызова, мы могли бы получать значение в теле вызываемого метода, изменив тип аргумента с T на Optional<,T>,:public void UseStream(Stream stream, Optional<,bool>, needClose){bool close = needClose.HasValue ? needClose.Value : true,// Или даже так bool close2 = needClose.HasValue ? needClose.Value : GetDefaultNeedClose(),}private static bool GetDefaultNeedClose(){return true,}
Именно такой подход применяется в языке F#:type CustomStreamWrapper() = let getNeedClose() = true // Необязательные аргументы можно использовать только в типах member public x.useStream (s: System.IO.Stream, ?needClose: bool) =let nc = defaultArg needClose (getNeedClose())// close stream if nc is true nc
Такой подход позволяет использовать выражения произвольной сложности для указания значения по умолчанию, хорошо работает с полиморфизмом (поскольку значение по умолчанию определяется во время исполнения, а не во время компиляции), но обладает своими недостатками.
Во-первых, значение по умолчанию становится неочевидным, поскольку его получение может быть в произвольном месте в коде метода.
Во-вторых, изменение простого аргумента на аргумент по умолчанию изменяет сигнатуру метода (ведь изменяется тип аргумента). В языках C++ и C# добавление значения по умолчанию аргументу не является ломающим изменением. В этом же случае, мы сломаем код, вызывающий метод через рефлексию, а также нам придется перекомпилировать всех клиентов этого метода. Это не страшно для промышленного кода, но может быть проблемным для библиотек.
UPDATE. Дополнительные ссылки
- C# Bug: Using Reflection with Default Properties
- Eric Lippert. Optional argument corner cases, part one
- Eric Lippert. Optional argument corner cases, part two
- Eric Lippert. Optional argument corner cases, part three
- Eric Lippert. Optional argument corner cases, part four