.NET / CLR의 API 버전 관리, 특히 API 변경으로 인해 클라이언트 응용 프로그램이 중단되는 방법에 관한 정보를 최대한 많이 수집하고 싶습니다. 먼저 몇 가지 용어를 정의하겠습니다.
API 변경 -공개 멤버를 포함한 유형의 공개적으로 정의 된 정의의 변경. 여기에는 유형 및 멤버 이름 변경, 유형의 기본 유형 변경, 구현 된 유형의 인터페이스 목록에서 인터페이스 추가 / 제거, 멤버 추가 / 제거 (오버로드 포함), 멤버 가시성 변경, 메소드 이름 변경 및 유형 매개 변수, 기본값 추가가 포함됩니다. 메소드 매개 변수, 유형 및 멤버에 속성 추가 / 제거 및 유형 및 멤버에 일반 유형 매개 변수 추가 / 제거 (아무것도 놓쳤습니까?) 여기에는 회원 기관의 변경 사항이나 개인 회원의 변경 사항이 포함되지 않습니다 (즉, 우리는 반영을 고려하지 않습니다).
이진 수준 중단 -이전 버전의 API에 대해 클라이언트 어셈블리가 컴파일되어 잠재적으로 새 버전으로로드되지 않는 API 변경입니다. 예 : 이전과 같은 방법으로 호출 할 수있는 경우에도 메소드 서명 변경 (예 : 유형 / 매개 변수 기본값 오버로드를 리턴하는 void)
소스 레벨 중단 -기존 버전의 API를 컴파일하기 위해 작성된 기존 코드가 새 버전으로 컴파일되지 않을 수있는 API 변경입니다. 그러나 이미 컴파일 된 클라이언트 어셈블리는 이전과 같이 작동합니다. 예 : 새로운 오버로드를 추가하여 이전에 모호하지 않은 메소드 호출이 모호해질 수 있습니다.
소스 레벨 자동 의미 변경 -이전 버전의 API에 대해 컴파일하기 위해 작성된 기존 코드로 인해 의미가 자동으로 변경 되는 API 변경 (예 : 다른 메소드 호출). 그러나 코드는 경고 / 오류없이 계속 컴파일되어야하며 이전에 컴파일 된 어셈블리는 이전과 같이 작동해야합니다. 예 : 기존 클래스에서 새 인터페이스를 구현하여 과부하 해결 중에 다른 과부하가 선택되었습니다.
궁극적 인 목표는 가능한 많은 중단 및 조용한 의미 론적 API 변경 사항을 카탈로그 화하고, 중단의 정확한 영향과 그 영향을받는 언어 및 언어를 설명하는 것입니다. 후자를 확장하기 위해 : 일부 변경 사항은 모든 언어에 보편적으로 영향을 미치지 만 (예 : 인터페이스에 새 멤버를 추가하면 모든 언어에서 해당 인터페이스의 구현이 중단 될 수 있음), 일부는 휴식을 취하기 위해 매우 구체적인 언어 의미론이 필요합니다. 여기에는 일반적으로 메서드 오버로드가 포함되며 일반적으로 암시 적 형식 변환과 관련된 모든 작업이 필요합니다. CLS 호환 언어 (예 : CLI 사양에 정의 된대로 “CLS 소비자”의 규칙을 준수하는 언어)에 대해서도 “최소 공통 분모”를 정의 할 수있는 방법이 없습니다. 누군가가 나를 잘못 여기로 고치면 감사하겠습니다. 그래서 언어별로 언어를 이동해야합니다. 가장 관심있는 것은 당연히 .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
메서드를 호출해야하기 때문에 F #은 손상되지 않습니다 .
새로운 인스턴스 메소드 추가
종류 : 소스 수준의 조용한 의미 체계 변경.
영향을받는 언어 : 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 확장 메소드를 정적 메소드로 호출해야 하므로 깨지지 않습니다 .
답변
메소드 서명 변경
종류 : 이진 수준의 휴식
영향을받는 언어 : 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 버전에 대해 다시 컴파일되지 않으면 도입 될 수있는 자동 2 진 레벨 중단이 더 나쁩니다. 열거 형 값은 컴파일 타임 상수이며 이러한 사용은 클라이언트 어셈블리의 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