Полное руководство по изменениям API в .NET

192

Я хотел бы собрать как можно больше информации об управлении версиями API в .NET/CLR, а также о том, как изменения API делают или не разрушают клиентские приложения. Сначала определим некоторые термины:

Изменение API - изменение общедоступного определения типа, включая любого из его публичных пользователей. Это включает в себя изменение типов и имен участников, изменение базового типа типа, добавление/удаление интерфейсов из списка реализованных интерфейсов типа, добавление/удаление элементов (включая перегрузки), изменение видимости элемента, метод переименования и параметры типа, добавление значений по умолчанию для параметров метода, добавления/удаления атрибутов для типов и элементов и добавления/удаления параметров типового типа для типов и элементов (я пропустил что-нибудь?). Это не включает никаких изменений в телах членов или каких-либо изменений в частных членах (т.е. Мы не учитываем Reflection).

Разрыв на двоичном уровне - изменение API, которое приводит к сбоям клиента, скомпилированным по сравнению со старой версией API, потенциально не загружаемой новой версией. Пример: изменение сигнатуры метода, даже если он позволяет вызываться так же, как и раньше (т.е.: Void для возврата значений значений по умолчанию для параметра/параметра по умолчанию).

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

Изменение тихой семантики на уровне исходного кода - изменение API, в результате чего существующий код, написанный для компиляции с более старой версией API, спокойно меняет свою семантику, например. путем вызова другого метода. Однако код должен продолжать компиляцию без предупреждений/ошибок, и ранее скомпилированные сборки должны работать по-прежнему. Пример: реализация нового интерфейса в существующем классе, в результате которого при перегрузке выбирается другая перегрузка.

Конечная цель состоит в том, чтобы каталогизировать как можно больше изменчивых и спокойных семантических API-интерфейсов, а также описывать точный эффект разлома, и какие языки не затрагиваются и не затрагиваются им. Чтобы расширить последнее: хотя некоторые изменения затрагивают все языки повсеместно (например, добавление нового элемента в интерфейс приведет к нарушению реализации этого интерфейса на любом языке), некоторые требуют очень специфической семантики языка для входа в игру, чтобы получить перерыв. Обычно это включает перегрузку методов и, в общем, что-то, что связано с неявными преобразованиями типов. Кажется, что нет никакого способа определить "наименьший общий знаменатель" здесь даже для CLS-совместимых языков (т.е. тех, которые соответствуют, по крайней мере, правилам "потребителя CLS", как определено в спецификации CLI), хотя я буду признателен, если кто-то исправляет меня как неправильный здесь - так что это должно будет идти языком по языку. Наиболее интересными являются, естественно, те, которые поставляются с .NET из коробки: С#, VB и F #; но другие, такие как IronPython, IronRuby, Delphi Prism и т.д. также актуальны. Чем больше углового случая, тем интереснее это будет - такие вещи, как удаление членов, довольно очевидны, но тонкие взаимодействия между, например, перегрузка метода, необязательные/параметры по умолчанию, вывод лямбда-типа и операторы преобразования могут быть очень неожиданными.

Несколько примеров, чтобы запустить это:

Добавление новых перегрузок методов

Вид: разрыв исходного уровня

Языки затронуты: С#, 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]);

Добавление новых неявных преобразований оператора

Вид: разрыв исходного уровня.

Языки затронуты: С#, 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.

Добавление новых методов экземпляра

Вид: тихая семантика на уровне исходного кода.

Языки затронуты: С#, 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 вызывались как статические методы.

  • 0
    Конечно, Microsoft уже покрывает это ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
  • 0
    @ Роберт: ваша ссылка о чем-то совсем другом - она описывает конкретные критические изменения в самой .NET Framework . Это более широкий вопрос, который описывает общие шаблоны, которые могут вносить серьезные изменения в ваши собственные API (как автор библиотеки / фреймворка). Я не знаю ни одного такого документа от MS, который был бы закончен, хотя любые ссылки на такие, даже если они неполные, определенно приветствуются.
Показать ещё 10 комментариев
Теги:
versioning
clr
cls-compliant

13 ответов

37
Лучший ответ

Изменение сигнатуры метода

Вид: разрыв двоичного уровня

Языки затронуты: С# (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);
  • 13
    На самом деле, это также может быть разрыв на уровне источника, если кто-то попытается создать делегата для bar .
  • 0
    Это тоже правда. Я обнаружил эту конкретную проблему, когда внес некоторые изменения в утилиты печати в приложении моей компании. Когда было выпущено обновление, не все библиотеки DLL, которые ссылались на эти утилиты, были перекомпилированы и выпущены, поэтому он вызывает исключение метода notfound.
Показать ещё 6 комментариев
32

Добавление параметра со значением по умолчанию.

Тип разрыва: разрыв двоичного уровня

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

Это потому, что С# компилирует значения параметров по умолчанию непосредственно в вызывающую сборку. Это означает, что если вы не перекомпилируете, вы получите исключение 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 ничего о них не знает. (Это также объясняет, почему значения по умолчанию должны быть константами времени компиляции в С#).

  • 1
    это серьезное изменение даже для уровня исходного кода: Func<int> f = Foo; // это не удастся с измененной подписью
22

Это было очень неочевидно, когда я его обнаружил, особенно в свете разницы с той же ситуацией для интерфейсов. Это не перерыв, но это удивительно, что я решил включить его:

Рефакторинг членов класса в базовый класс

Вид: не перерыв!

Языки затронуты: ни один (т.е. ни один не сломан)

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

Примечания:

С++/CLI - это единственный язык .NET, который имеет конструкцию, аналогичную явной реализации интерфейса для членов виртуального базового класса - "явное переопределение". Я полностью ожидал, что это приведет к тому же поломке, что и при перемещении элементов интерфейса к базовому интерфейсу (поскольку IL, сгенерированный для явного переопределения, такой же, как для явной реализации). К моему удивлению, это не так, даже если сгенерированный IL по-прежнему указывает, что BarOverride переопределяет Foo::Bar, а не FooBase::Bar, загрузчик сборок достаточно умен, чтобы правильно заменить один на другой без каких-либо жалоб - по-видимому, тот факт, что Foo - это класс, в чем разница. Перейти фигурой...

  • 3
    Пока базовый класс находится в одной сборке. В противном случае это двоичное изменение.
  • 0
    @ Джереми, какой код ломается в этом случае? Будет ли использование Baz () внешним вызывающим пользователем прервано или это проблема только людей, которые пытаются расширить Foo и переопределить Baz ()?
Показать ещё 1 комментарий
17

Это, возможно, не столь очевидный частный случай "добавления/удаления элементов интерфейса", и я решил, что он заслуживает собственной записи в свете другого случая, который я собираюсь опубликовать дальше. Итак:

Рефакторинг элементов интерфейса в базовый интерфейс

Вид: разбивается как на исходный, так и на двоичный уровни

Языки затронуты: С#, VB, С++/CLI, F # (для исходного break; двоичного, естественно, влияет на любой язык)

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();

Примечания:

Для разрыва исходного уровня проблема в том, что для С#, VB и С++/CLI требуется точное имя интерфейса в объявлении реализации члена интерфейса; таким образом, если элемент перемещается в базовый интерфейс, код больше не будет компилироваться.

Двоичный разрыв связан с тем, что методы интерфейса полностью определены в сгенерированном IL для явных реализаций, а также имя интерфейса также должно быть точным.

Неявная реализация там, где она доступна (т.е. С# и С++/CLI, но не VB) будет нормально работать как на уровне источника, так и на двоичном уровне. Вызов метода также не прерывается.

  • 0
    Это не верно для всех языков. Для VB это не смена исходного кода. Для C # это так.
  • 0
    Так что Implements IFoo.Bar будет прозрачно ссылаться на IFooBase.Bar ?
Показать ещё 1 комментарий
11

Переупорядочение нумерованных значений

Вид разрыва: Изменение тихой семантики исходного уровня/двоичного уровня

Языки затронуты: все

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

Еще хуже, что тихие разрывы двоичного уровня могут быть введены, если код клиента не перекомпилирован в отношении новой версии API. Значения Enum являются константами времени компиляции, и поэтому их использование испекает в клиентской сборке IL. Этот случай может быть особенно трудным определить время от времени.

API до изменения

public enum Foo
{
   Bar,
   Baz
}

API после изменения

public enum Foo
{
   Baz,
   Bar
}

Пример клиентского кода, который работает, но затем разбивается:

Foo.Bar < Foo.Baz
10

Это действительно очень редко на практике, но тем не менее удивительно, когда это происходит.

Добавление новых неперегруженных элементов

Вид: изменение уровня источника или тихая семантика.

Языки затронуты: С#, VB

Языки не затронуты: F #, С++/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());        
    }
}

Примечания:

Проблема здесь вызвана выводами лямбда-типа в С# и VB в присутствии разрешения перегрузки. Здесь используется ограниченная форма утиного набора текста, чтобы разбить связи, в которых встречается более одного типа, проверяя, имеет ли тело лямбда смысл для данного типа - если только один тип приводит к компилируемому телу, этот выбран.

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

Обратите внимание, что типы Foo и Bar в этом примере никак не связаны, а не наследованием или иным образом. Простое использование их в одной группе методов достаточно, чтобы вызвать это, и если это происходит в клиентском коде, вы не контролируете его.

Пример кода выше демонстрирует более простую ситуацию, когда это перерыв на уровне источника (т.е. результаты ошибки компилятора). Однако это также может быть молчаливое изменение семантики, если перегрузка, выбранная с помощью вывода, имела другие аргументы, которые в противном случае могли бы быть ранжированы ниже (например, необязательные аргументы со значениями по умолчанию или несоответствие типов между объявленным и фактическим аргументом, требующим неявного преобразование). В таком сценарии разрешение перегрузки больше не будет терпеть неудачу, но другой компилятор будет спокойно выбирать другую перегрузку. На практике, однако, очень трудно столкнуться с этим случаем без тщательного построения сигнатур методов, чтобы умышленно вызвать его.

7

Преобразовать реализацию неявного интерфейса в явный.

Вид разрыва: источник и двоичный

Затрагиваемые языки: все

Это действительно просто изменение возможности доступа к методу - это немного более тонко, поскольку легко упустить из виду тот факт, что не все доступ к методам интерфейса обязательно связаны с типом интерфейса.

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
5

Изменение поля для свойства

Вид разрыва: API

Затрагиваемые языки: Visual Basic и С# *

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

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"
  • 2
    На самом деле это также нарушает работу C #, потому что свойства не могут использоваться для аргументов out и ref методов, в отличие от полей, и не могут быть целью унарного оператора & .
  • 0
    Спасибо, Павел, я добавлю это к записи.
5

Преобразование явной реализации интерфейса в неявный.

Вид разрыва: Источник

Затрагиваемые языки: все

Рефакторинг явной реализации интерфейса в неявный является более тонким в том, как он может нарушить 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"
  • 0
    Извините, я не совсем следую - конечно, пример кода до изменения API не будет компилироваться вообще, так как до изменения у Foo не было открытого метода с именем GetEnumerator , и вы вызываете метод через ссылку типа Foo ...
  • 0
    На самом деле, я попытался упростить пример из памяти, и он закончился «foobar» (простите за каламбур). Я обновил пример, чтобы правильно продемонстрировать случай (и быть компилируемым).
Показать ещё 2 комментария
4

Добавление пространства имен

Изменение уровня исходного уровня/изменение тишины на уровне исходного уровня

Из-за того, как разрешение пространства имен работает в 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 version 1?) и С#.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, который ссылается на 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);
3

Изменение 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)

Изменения в Bootstrap/Configuration:

  • Добавление/удаление/изменение пользовательских параметров конфигурации (например, настроек App.config)
  • При интенсивном использовании IoC/DI в сегодняшних приложениях необходимо что-то изменить для изменения и/или изменения кода начальной загрузки для кода, зависящего от DI.

Update:

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

  • 0
    «Добавление нового члена (защищенного от событий), который использует тип из другой сборки». - IIRC, клиент должен ссылаться только на зависимые сборки, которые содержат базовые типы сборок, на которые он уже ссылается; он не должен ссылаться на сборки, которые просто используются (даже если типы находятся в сигнатурах методов); Я не уверен на 100% в этом. У вас есть ссылка для точных правил для этого? Кроме того, перемещение типа может быть неразрывным, если используется TypeForwardedToAttribute .
  • 0
    Это "TypeForwardedTo" для меня новость, я проверю это. Что касается другого, я также не на 100% на этом ... позвольте мне видеть, может ли repro, и я обновлю почту.
Показать ещё 2 комментария
2

Добавление методов перегрузки для снижения использования параметров по умолчанию

Вид разрыва: Изменение тихой семантики исходного уровня

Поскольку компилятор преобразует вызовы методов с отсутствующими значениями параметров по умолчанию для явного вызова со значением по умолчанию на вызывающей стороне, предоставляется совместимость для существующего скомпилированного кода; метод с правильной сигнатурой будет найден для всего ранее скомпилированного кода.

С другой стороны, вызовы без использования необязательных параметров теперь скомпилированы как вызов нового метода, в котором отсутствует необязательный параметр. Все это работает нормально, но если вызываемый код находится в другой сборке, новый скомпилированный код, вызывающий его, теперь зависит от новой версии этой сборки. Развертывание сборок, вызывающих рефакторизованный код, без развертывания сборки, в которой находится реорганизованный код, приводит к исключениям "метод не найден".

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

Переименование интерфейса

Вид разрыва: источник и Двоичный

Затрагиваемые языки: скорее всего все, протестированные на С#.

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню