Полное руководство по API-изменениям в.NET
Я хотел бы собрать как можно больше информации о версиях API в.NET/CLR и, в частности, о том, как изменения API нарушают или не нарушают клиентские приложения. Сначала давайте определим некоторые термины:
Изменение API - изменение в общедоступном определении типа, включая любого из его открытых членов. Это включает в себя изменение типа и имен элементов, изменение базового типа типа, добавление / удаление интерфейсов из списка реализованных интерфейсов типа, добавление / удаление элементов (включая перегрузки), изменение видимости элемента, переименование метода и параметров типа, добавление значений по умолчанию для параметров метода, добавление / удаление атрибутов для типов и членов, а также добавление / удаление параметров общих типов для типов и членов (я что-то пропустил?). Это не включает какие-либо изменения в членских органах или какие-либо изменения в частных членах (т.е. мы не принимаем во внимание Рефлексию).
Разрыв двоичного уровня - изменение API, в результате которого клиентские сборки, скомпилированные с более старой версией API, потенциально не загружаются с новой версией. Пример: изменение сигнатуры метода, даже если он позволяет вызываться так же, как и раньше (то есть: void для возврата значений по умолчанию для значений типа / параметра).
Разрыв на уровне исходного кода - изменение API, в результате которого существующий код, написанный для компиляции со старой версией API, потенциально не компилируется с новой версией. Однако уже скомпилированные клиентские сборки работают как и прежде. Пример: добавление новой перегрузки, которая может привести к неоднозначности в вызовах методов, которые были однозначными в предыдущем.
Изменение тихой семантики на уровне исходного кода - изменение API, в результате которого существующий код, написанный для компиляции с более старой версией API, незаметно меняет свою семантику, например, путем вызова другого метода. Однако код должен продолжать компилироваться без предупреждений / ошибок, а ранее скомпилированные сборки должны работать как прежде. Пример: реализация нового интерфейса в существующем классе, что приводит к другой перегрузке, выбранной во время разрешения перегрузки.
Конечная цель состоит в том, чтобы каталогизировать как можно больше ломающих и тихих изменений API семантики и описать точный эффект поломки, а также то, какие языки влияют на него и не затрагиваются им. Чтобы расширить последний: хотя некоторые изменения повлияют на все языки повсеместно (например, добавление нового члена в интерфейс нарушит реализацию этого интерфейса на любом языке), некоторые требуют очень специфической семантики языка, чтобы вступить в игру, чтобы получить разрыв. Это обычно включает перегрузку методов и, вообще, все, что связано с неявными преобразованиями типов. Кажется, что нет никакого способа определить "наименее общий знаменатель" здесь даже для CLS-совместимых языков (то есть тех, которые соответствуют по крайней мере правилам "потребителя CLS", как определено в спецификации CLI) - хотя я буду признателен, если кто-то исправляет меня здесь как ошибку - так что это должно идти от языка к языку. Естественно, наиболее интересны те, которые поставляются с.NET из коробки: C#, VB и F#; но другие, такие как IronPython, IronRuby, Delphi Prism и т. д. также актуальны. Чем больше это угловой ситуации, тем интереснее это будет - такие вещи, как удаление элементов, довольно очевидны, но тонкое взаимодействие между, например, перегрузкой метода, необязательными параметрами / параметрами по умолчанию, выводом типа лямбда-выражения и операторами преобразования может быть очень удивительным во время.
Вот несколько примеров, чтобы начать это:
Добавление нового метода перегрузки
Вид: разрыв на уровне источника
Затрагиваемые языки: C#, VB, F#
API до изменения:
public class Foo
{
public void Bar(IEnumerable x);
}
API после изменения:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Пример клиентского кода, работающего до изменения и сломанного после него:
new Foo().Bar(new int[0]);
Добавление новых неявных перегрузок оператора преобразования
Вид: разрыв на уровне источника.
Затрагиваемые языки: C#, VB
Языки, не затронутые: F#
API до изменения:
public class Foo
{
public static implicit operator int ();
}
API после изменения:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Пример клиентского кода, работающего до изменения и сломанного после него:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Примечания: F# не сломан, потому что он не имеет никакой поддержки на уровне языка для перегруженных операторов, ни явных, ни неявных - оба должны вызываться напрямую как op_Explicit
а также op_Implicit
методы.
Добавление новых методов экземпляра
Вид: тихая семантика исходного уровня изменяется.
Затрагиваемые языки: C#, VB
Языки, не затронутые: F#
API до изменения:
public class Foo
{
}
API после изменения:
public class Foo
{
public void Bar();
}
Пример клиентского кода, который подвергается тихому изменению семантики:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Примечания: F# не сломан, потому что у него нет поддержки уровня языка ExtensionMethodAttribute
и требует, чтобы методы расширения CLS вызывались как статические методы.
13 ответов
Изменение подписи метода
Вид: разрыв двоичного уровня
Затрагиваемые языки: C# (VB и F# скорее всего, но не проверено)
API до изменения
public static class Foo
{
public static void bar(int i);
}
API после изменения
public static class Foo
{
public static bool bar(int i);
}
Пример кода клиента, работающего до изменения
Foo.bar(13);
Добавление параметра со значением по умолчанию.
Вид разрыва: разрыв двоичного уровня
Даже если исходный код вызова не нужно менять, его все равно нужно перекомпилировать (как при добавлении обычного параметра).
Это связано с тем, что C# компилирует значения параметров по умолчанию непосредственно в вызывающую сборку. Это означает, что если вы не перекомпилируете, вы получите MissingMethodException, потому что старая сборка пытается вызвать метод с меньшим количеством аргументов.
API до изменения
public void Foo(int a) { }
API после изменения
public void Foo(int a, string b = null) { }
Образец клиентского кода, который впоследствии нарушается
Foo(5);
Код клиента должен быть перекомпилирован в Foo(5, null)
на уровне байт-кода. Вызываемая сборка будет содержать только Foo(int, string)
не Foo(int)
, Это связано с тем, что значения параметров по умолчанию являются чисто языковой функцией, а среда выполнения.Net о них ничего не знает. (Это также объясняет, почему значения по умолчанию должны быть константами времени компиляции в C#).
Когда я его обнаружил, он был совершенно неочевиден, особенно в свете различий с той же ситуацией для интерфейсов. Это вовсе не перерыв, но достаточно удивительно, что я решил включить его:
Рефакторинг членов класса в базовый класс
Вид: не перерыв!
Затрагиваемые языки: нет (т.е. никто не сломан)
API до изменения:
class Foo
{
public virtual void Bar() {}
public virtual void Baz() {}
}
API после изменения:
class FooBase
{
public virtual void Bar() {}
}
class Foo : FooBase
{
public virtual void Baz() {}
}
Пример кода, который продолжает работать на протяжении всего изменения (хотя я ожидал, что он сломается):
// C++/CLI
ref class Derived : Foo
{
public virtual void Baz() {{
// Explicit override
public virtual void BarOverride() = Foo::Bar {}
};
Заметки:
C++ / CLI - единственный язык.NET, имеющий конструкцию, аналогичную явной реализации интерфейса для членов виртуального базового класса - "явное переопределение". Я полностью ожидал, что это приведет к тому же виду поломки, что и при перемещении элементов интерфейса к базовому интерфейсу (поскольку IL, сгенерированный для явного переопределения, такой же, как и для явной реализации). К моему удивлению, это не так - хотя сгенерированный IL все еще указывает, что BarOverride
Переопределение Foo::Bar
скорее, чем FooBase::Bar
, сборочный загрузчик достаточно умен, чтобы правильно заменить один на другой без нареканий - видимо, тот факт, что Foo
это класс, что имеет значение. Пойди разберись...
Это, возможно, не столь очевидный особый случай "добавления / удаления членов интерфейса", и я решил, что он заслуживает отдельной записи в свете другого случая, который я собираюсь опубликовать в следующем. Так:
Рефакторинг членов интерфейса в базовый интерфейс
Вид: разрывы на исходном и двоичном уровнях
Затрагиваемые языки: C#, VB, C++/CLI, F# (для разрыва источника; двоичный естественно влияет на любой язык)
API до изменения:
interface IFoo
{
void Bar();
void Baz();
}
API после изменения:
interface IFooBase
{
void Bar();
}
interface IFoo : IFooBase
{
void Baz();
}
Пример кода клиента, который нарушается при изменении на уровне источника:
class Foo : IFoo
{
void IFoo.Bar() { ... }
void IFoo.Baz() { ... }
}
Пример кода клиента, который нарушается при изменении на двоичном уровне;
(new Foo()).Bar();
Заметки:
Для разрыва исходного уровня проблема состоит в том, что C#, VB и C++ / CLI требуют точного имени интерфейса в объявлении реализации элемента интерфейса; таким образом, если член перемещается в базовый интерфейс, код больше не будет компилироваться.
Двоичный разрыв связан с тем, что методы интерфейса полностью определены в сгенерированном IL для явных реализаций, и имя интерфейса там также должно быть точным.
Неявная реализация, где она доступна (например, C# и C++ / CLI, но не VB), будет хорошо работать как на исходном, так и на двоичном уровне. Вызовы методов также не прерываются.
Изменение порядка перечисляемых значений
Вид перерыва: Изменение семантики тихого семантики на уровне источника / двоичного уровня
Затрагиваемые языки: все
Переупорядочение перечисляемых значений сохранит совместимость на уровне источника, поскольку литералы имеют одинаковые имена, но их порядковые индексы будут обновлены, что может вызвать некоторые виды молчаливых разрывов на уровне источника.
Еще хуже то, что тихие разрывы двоичного уровня могут быть введены, если клиентский код не перекомпилируется с новой версией API. Значения enum являются константами времени компиляции, и любое их использование запекается в IL клиентской сборки. Этот случай может быть особенно трудно заметить время от времени.
API до изменения
public enum Foo
{
Bar,
Baz
}
API после изменения
public enum Foo
{
Baz,
Bar
}
Пример клиентского кода, который работает, но впоследствии не работает:
Foo.Bar < Foo.Baz
Это действительно очень редкая вещь на практике, но, тем не менее, удивительно, когда это происходит.
Добавление новых не перегруженных участников
Вид: разрыв исходного уровня или тихая смена семантики.
Затрагиваемые языки: C#, VB
Языки, не затронутые: F#, C++ / CLI
API до изменения:
public class Foo
{
}
API после изменения:
public class Foo
{
public void Frob() {}
}
Пример кода клиента, который нарушается при изменении:
class Bar
{
public void Frob() {}
}
class Program
{
static void Qux(Action<Foo> a)
{
}
static void Qux(Action<Bar> a)
{
}
static void Main()
{
Qux(x => x.Frob());
}
}
Заметки:
Проблема здесь вызвана выводом лямбда-типа в C# и VB при наличии разрешения перегрузки. Ограниченная форма утиной типизации используется здесь, чтобы разорвать связи, где более чем один тип соответствует, проверяя, имеет ли смысл тело лямбды для данного типа - если только один тип приводит к компилируемому телу, этот выбирается.
Опасность заключается в том, что в клиентском коде может быть перегруженная группа методов, где некоторые методы принимают аргументы своих собственных типов, а другие принимают аргументы типов, предоставляемых вашей библиотекой. Если какой-либо из его кодов затем использует алгоритм вывода типов для определения правильного метода, основанного исключительно на наличии или отсутствии членов, то добавление нового члена к одному из ваших типов с тем же именем, что и в одном из типов клиента, может потенциально вызвать вывод выкл, что приводит к неоднозначности при разрешении перегрузки.
Обратите внимание, что типы Foo
а также Bar
в этом примере не связаны ни каким-либо образом, ни по наследству, ни по-другому. Простого использования их в одной группе методов достаточно, чтобы вызвать это, и если это происходит в клиентском коде, вы не можете его контролировать.
Приведенный выше пример кода демонстрирует более простую ситуацию, когда это разрыв на уровне исходного кода (то есть результаты ошибки компилятора). Тем не менее, это также может быть автоматическим изменением семантики, если перегрузка, которая была выбрана с помощью логического вывода, имела другие аргументы, которые иначе привели бы к ее ранжированию ниже (например, необязательные аргументы со значениями по умолчанию или несоответствие типов между объявленным и фактическим аргументом, требующим неявного преобразование). В таком случае разрешение перегрузки больше не будет терпеть неудачу, но компилятор будет спокойно выбирать другую перегрузку. На практике, однако, очень трудно столкнуться с этим случаем без тщательного построения сигнатур методов, чтобы преднамеренно вызывать его.
Преобразуйте неявную реализацию интерфейса в явную.
Вид разрыва: источник и бинарный
Затрагиваемые языки: все
На самом деле это всего лишь вариант изменения доступности метода - он немного более тонкий, поскольку легко упустить из виду тот факт, что не весь доступ к методам интерфейса обязательно осуществляется через ссылку на тип интерфейса.
API до изменения:
public class Foo : IEnumerable
{
public IEnumerator GetEnumerator();
}
API после изменения:
public class Foo : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator();
}
Пример кода клиента, который работает до изменения и впоследствии не работает:
new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public
Изменение поля на свойство
Вид перерыва: API
Затрагиваемые языки: Visual Basic и C#*
Информация: Когда вы заменяете обычное поле или переменную на свойство в Visual Basic, любой внешний код, ссылающийся на этот элемент каким-либо образом, необходимо будет перекомпилировать.
API до изменения:
Public Class Foo
Public Shared Bar As String = ""
End Class
API после изменения:
Public Class Foo
Private Shared _Bar As String = ""
Public Shared Property Bar As String
Get
Return _Bar
End Get
Set(value As String)
_Bar = value
End Set
End Property
End Class
Пример клиентского кода, который работает, но впоследствии не работает:
Foo.Bar = "foobar"
Преобразуйте явную реализацию интерфейса в неявную.
Вид разрыва: Источник
Затрагиваемые языки: все
Реорганизация явной реализации интерфейса в неявную более тонка в том, как она может сломать API. На первый взгляд может показаться, что это должно быть относительно безопасно, однако в сочетании с наследованием это может вызвать проблемы.
API до изменения:
public class Foo : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}
API после изменения:
public class Foo : IEnumerable
{
public IEnumerator GetEnumerator() { yield return "Foo"; }
}
Пример кода клиента, который работает до изменения и впоследствии не работает:
class Bar : Foo, IEnumerable
{
IEnumerator IEnumerable.GetEnumerator() // silently hides base instance
{ yield return "Bar"; }
}
foreach( var x in new Bar() )
Console.WriteLine(x); // originally output "Bar", now outputs "Foo"
Дополнение пространства имен
Разрыв на уровне источника / изменение тихой семантики на уровне источника
Из-за способа разрешения пространства имен в vb.Net добавление пространства имен в библиотеку может привести к тому, что код Visual Basic, скомпилированный с предыдущей версией API, не скомпилируется с новой версией.
Пример кода клиента:
Imports System
Imports Api.SomeNamespace
Public Class Foo
Public Sub Bar()
Dim dr As Data.DataRow
End Sub
End Class
Если новая версия API добавляет пространство имен Api.SomeNamespace.Data
, то приведенный выше код не будет компилироваться.
Это становится более сложным с импортом пространства имен на уровне проекта. Если Imports System
опущен в приведенном выше коде, но System
Пространство имен импортируется на уровне проекта, тогда код все равно может привести к ошибке.
Тем не менее, если API включает в себя класс DataRow
в его Api.SomeNamespace.Data
пространство имен, то код будет скомпилирован, но dr
будет примером System.Data.DataRow
при компиляции со старой версией API и Api.SomeNamespace.Data.DataRow
при компиляции с новой версией API.
Аргумент Переименование
Разрыв уровня источника
Изменение имен аргументов - это серьезное изменение в vb.net с версии 7(?) (.Net версия 1?) И C#.net с версии 4 (.Net версия 4).
API до изменения:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
}
}
API после изменения:
namespace SomeNamespace {
public class Foo {
public static void Bar(string y) {
...
}
}
}
Пример кода клиента:
Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB
Параметры Ref
Разрыв уровня источника
Добавление переопределения метода с той же сигнатурой, за исключением того, что один параметр передается по ссылке, а не по значению, приведет к тому, что vb source, который ссылается на API, не сможет разрешить функцию. Visual Basic не имеет возможности (?) Дифференцировать эти методы в точке вызова, если они не имеют разных имен аргументов, поэтому такое изменение может привести к невозможности использования обоих членов из кода VB.
API до изменения:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
}
}
API после изменения:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
public static void Bar(ref string x) {
...
}
}
}
Пример кода клиента:
Api.SomeNamespace.Foo.Bar(str)
Поле для изменения свойства
Разрыв двоичного уровня / Разрыв исходного уровня
Помимо очевидного разрыва двоичного уровня, это может вызвать разрыв уровня источника, если элемент передается методу по ссылке.
API до изменения:
namespace SomeNamespace {
public class Foo {
public int Bar;
}
}
API после изменения:
namespace SomeNamespace {
public class Foo {
public int Bar { get; set; }
}
}
Пример кода клиента:
FooBar(ref Api.SomeNamespace.Foo.Bar);
Изменение API:
- Добавление атрибута [Устаревший] (вы как бы покрыли это упоминанием атрибутов; однако это может быть критически важным изменением при использовании предупреждения об ошибке.)
Разрыв двоичного уровня:
- Перемещение типа из одной сборки в другую
- Изменение пространства имен типа
- Добавление типа базового класса из другой сборки.
Добавление нового члена (защищенного от событий), который использует тип из другой сборки (Class2) в качестве ограничения аргумента шаблона.
protected void Something<T>() where T : Class2 { }
Изменение дочернего класса (Class3) для наследования от типа в другой сборке, когда класс используется в качестве аргумента шаблона для этого класса.
protected class Class3 : Class2 { } protected void Something<T>() where T : Class3 { }
Изменение тихой семантики на уровне источника:
- Добавление / удаление / изменение переопределений Equals(), GetHashCode() или ToString ()
(не уверен, где они подходят)
Изменения развертывания:
- Добавление / удаление зависимостей / ссылок
- Обновление зависимостей до более новых версий
- Изменение целевой платформы между x86, Itanium, x64 или anycpu
- Сборка / тестирование на другой установке фреймворка (т. Е. Установка 3.5 на коробку.Net 2.0 разрешает вызовы API, которые затем требуют.Net 2.0 SP2)
Начальная загрузка / Изменения конфигурации:
- Добавление / удаление / изменение пользовательских параметров конфигурации (например, параметров App.config)
- В связи с интенсивным использованием IoC/DI в современных приложениях необходимо переконфигурировать и / или изменить код начальной загрузки для кода, зависимого от DI.
Обновить:
Извините, я не осознавал, что единственная причина, по которой это сломалось, заключалась в том, что я использовал их в шаблонных ограничениях.
Добавление методов перегрузки для прекращения использования параметров по умолчанию
Вид разрыва: изменение семантики тихого уровня источника
Поскольку компилятор преобразует вызовы методов с отсутствующими значениями параметров по умолчанию в явный вызов со значением по умолчанию на вызывающей стороне, обеспечивается совместимость для существующего скомпилированного кода; метод с правильной сигнатурой будет найден для всего ранее скомпилированного кода.
С другой стороны, вызовы без использования необязательных параметров теперь компилируются как вызов нового метода, в котором отсутствует необязательный параметр. Все по-прежнему работает нормально, но если вызываемый код находится в другой сборке, то вновь скомпилированный код, вызывающий его, теперь зависит от новой версии этой сборки. Развертывание сборок, вызывающих реорганизованный код, без развертывания сборки, в которой находится реорганизованный код, приводит к исключениям "метод не найден".
API до изменения
public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
{
return mandatoryParameter + optionalParameter;
}
API после изменения
public int MyMethod(int mandatoryParameter, int optionalParameter)
{
return mandatoryParameter + optionalParameter;
}
public int MyMethod(int mandatoryParameter)
{
return MyMethod(mandatoryParameter, 0);
}
Пример кода, который все еще будет работать
public int CodeNotDependentToNewVersion()
{
return MyMethod(5, 6);
}
Пример кода, который теперь зависит от новой версии при компиляции
public int CodeDependentToNewVersion()
{
return MyMethod(5);
}
Переименование интерфейса
Вид разрыва: источник и бинарный
Затрагиваемые языки: Скорее всего все, протестировано на C#.
API до изменения:
public interface IFoo
{
void Test();
}
public class Bar
{
IFoo GetFoo() { return new Foo(); }
}
API после изменения:
public interface IFooNew // Of the exact same definition as the (old) IFoo
{
void Test();
}
public class Bar
{
IFooNew GetFoo() { return new Foo(); }
}
Пример клиентского кода, который работает, но впоследствии не работает:
new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break
Продвижение в метод продления
Вид: разрыв на уровне источника
Затрагиваемые языки: C# v6 и выше (может быть, другие?)
API до изменения:
public static class Foo
{
public static void Bar(string x);
}
API после изменения:
public static class Foo
{
public void Bar(this string x);
}
Пример кода клиента, работающего до изменения и прерываемого после него:
using static Foo;
class Program
{
static void Main() => Bar("hello");
}
Дополнительная информация: https://github.com/dotnet/csharplang/issues/665
Метод перегрузки с параметром обнуляемого типа
Вид: разрыв на уровне источника
Затронутые языки: C#, VB
API до изменения:
public class Foo
{
public void Bar(string param);
}
API после изменения:
public class Foo
{
public void Bar(string param);
public void Bar(int? param);
}
Пример клиентского кода, работающего до изменения и сломанного после него:
new Foo().Bar(null);
Исключение: неоднозначный вызов следующих методов или свойств.
Статическое преобразование только для чтения в const
Вид: Перерыв на двоичном уровне
Затронутые языки: C#, VB и F#.
API до изменения:
public static class Foo
{
public static readonly string Bar = "Value";
}
API после изменения:
public static class Foo
{
public const string Bar = "Value";
}
Все клиенты должны быть перекомпилированы, чтобы настроить таргетинг на новое изменение, в противном случае
MissingFieldException
брошен.
Расширение Visual Studio NDepend предоставляет несколько правил в категории "Критические изменения API" для обнаружения разрыва двоичного уровня. Эти правила выполняются, только если определена базовая линия NDepend.
- Критические изменения API: Типы: это правило предупреждает, если тип, публично видимый в базовой линии, больше не является общедоступным или был удален. Код клиентов, использующих такой тип, будет нарушен.
- Критические изменения API: методы: это правило предупреждает, если метод, публично видимый в базовой линии, больше не является общедоступным или был удален. Код клиентов, использующих такой метод, будет нарушен. Обратите внимание, что если подпись метода изменяется, старая версия метода рассматривается как удаленная, а новая версия метода - как добавленная, поэтому критическое изменение будет обнаружено в старой версии метода.
- Критические изменения API: Поля: это правило предупреждает, если поле, общедоступное в базовой линии, больше не является общедоступным или было удалено. Код клиента, использующий такое поле, будет нарушен.
- Критические изменения API: интерфейсы и абстрактные классы: это правило предупреждает, если общедоступный интерфейс или абстрактный класс был изменен и содержит новые абстрактные методы, или если некоторые абстрактные методы были удалены. Клиентский код, реализующий такой интерфейс или производный от такого абстрактного класса, будет нарушен.
- Нарушенные сериализуемые типы: это правило предупреждает о нарушении изменений в типах, помеченных SerializableAttribute. Для этого это правило ищет сериализуемый тип с сериализуемыми полями экземпляра, добавленными или удаленными с базового уровня. Обратите внимание, что он не принимает во внимание поля с тегами NonSerializedAttribute.
- Избегайте изменения перечислений. Состояние флагов: это правило соответствует типам перечислений, которые раньше помечались тегами FlagsAttribute в базовой линии, а не больше. Он также соответствует противоположным типам перечисления, которые теперь помечены FlagsAttribute и не помечены в базовой линии. Наличие тега FlagsAttribute - сильное свойство перечисления. Не столько с точки зрения поведения (только поведение метода enum.ToString() изменяется, когда перечисление помечено FlagsAttribute), но с точки зрения смысла: является ли перечисление диапазоном значений или диапазоном флагов?
Также предлагается 3 запроса кода, позволяющие пользователю просматривать новые элементы общедоступного API: