Hướng dẫn dứt khoát về các thay đổi phá vỡ API trong .NET


227

Tôi muốn thu thập càng nhiều thông tin càng tốt về phiên bản API trong .NET / CLR và cụ thể cách thay đổi API thực hiện hoặc không phá vỡ các ứng dụng khách. Đầu tiên, hãy xác định một số thuật ngữ:

Thay đổi API - một thay đổi trong định nghĩa hiển thị công khai của một loại, bao gồm bất kỳ thành viên nào. Điều này bao gồm thay đổi tên thành viên và loại thành viên, thay đổi loại cơ sở của loại, thêm / xóa giao diện khỏi danh sách các giao diện đã triển khai của loại, thêm / xóa thành viên (bao gồm quá tải), thay đổi mức độ hiển thị của thành viên, đổi tên phương thức và tham số loại, thêm giá trị mặc định cho các tham số phương thức, thêm / xóa thuộc tính trên các loại và thành viên và thêm / xóa tham số loại chung trên các loại và thành viên (tôi có bỏ sót điều gì không?). Điều này không bao gồm bất kỳ thay đổi nào trong các cơ quan thành viên hoặc bất kỳ thay đổi nào đối với các thành viên tư nhân (nghĩa là chúng tôi không tính đến Reflection).

Ngắt cấp nhị phân - một thay đổi API dẫn đến các cụm máy khách được biên dịch theo phiên bản API cũ hơn có khả năng không tải với phiên bản mới. Ví dụ: thay đổi chữ ký phương thức, ngay cả khi nó cho phép được gọi theo cùng một cách như trước đây (nghĩa là: void để trả về giá trị mặc định của loại / tham số mặc định).

Ngắt cấp độ nguồn - một thay đổi API dẫn đến mã hiện tại được viết để biên dịch theo phiên bản cũ hơn của API có khả năng không biên dịch với phiên bản mới. Tuy nhiên, đã biên dịch các cụm máy khách hoạt động như trước đây. Ví dụ: thêm một quá tải mới có thể dẫn đến sự mơ hồ trong các cuộc gọi phương thức không rõ ràng trước đó.

Thay đổi ngữ nghĩa yên tĩnh ở mức nguồn - một thay đổi API dẫn đến mã hiện tại được viết để biên dịch theo phiên bản cũ hơn của API lặng lẽ thay đổi ngữ nghĩa của nó, ví dụ bằng cách gọi một phương thức khác. Tuy nhiên, mã nên tiếp tục biên dịch mà không có cảnh báo / lỗi và các hội đồng được biên dịch trước đó sẽ hoạt động như trước. Ví dụ: triển khai giao diện mới trên một lớp hiện có dẫn đến tình trạng quá tải khác nhau được chọn trong quá trình phân giải quá tải.

Mục tiêu cuối cùng là lập danh mục càng nhiều thay đổi API ngữ nghĩa phá vỡ và yên tĩnh càng tốt và mô tả chính xác hiệu quả của sự phá vỡ, và ngôn ngữ nào và không bị ảnh hưởng bởi nó. Để mở rộng về sau: trong khi một số thay đổi ảnh hưởng đến tất cả các ngôn ngữ trên toàn cầu (ví dụ: thêm thành viên mới vào giao diện sẽ phá vỡ việc triển khai giao diện đó bằng bất kỳ ngôn ngữ nào), một số yêu cầu ngữ nghĩa ngôn ngữ rất cụ thể để tham gia để nghỉ ngơi. Điều này điển hình nhất liên quan đến quá tải phương thức và, nói chung, bất cứ điều gì phải làm với chuyển đổi kiểu ngầm định. Dường như không có cách nào để định nghĩa "mẫu số chung nhỏ nhất" ở đây ngay cả đối với các ngôn ngữ tuân thủ CLS (nghĩa là các ngôn ngữ tuân thủ ít nhất theo quy tắc của "người tiêu dùng CLS" như được định nghĩa trong thông số CLI) - mặc dù tôi ' sẽ đánh giá cao nếu ai đó sửa tôi là sai ở đây - vì vậy điều này sẽ phải đi theo ngôn ngữ. Những người quan tâm nhất đương nhiên là những người đi kèm với .NET: C #, VB và F #; nhưng những thứ khác, chẳng hạn như IronPython, IronRuby, Delphi Prism v.v ... cũng có liên quan. Càng nhiều trường hợp góc, nó sẽ càng thú vị hơn - những thứ như loại bỏ các thành viên khá rõ ràng, nhưng các tương tác tinh tế giữa quá tải phương thức, tham số tùy chọn / mặc định, suy luận kiểu lambda và toán tử chuyển đổi có thể rất đáng ngạc nhiên nhiều lúc

Một vài ví dụ để khởi động điều này:

Thêm quá tải phương thức mới

Loại: phá vỡ mức nguồn

Ngôn ngữ bị ảnh hưởng: C #, VB, F #

API trước khi thay đổi:

public class Foo
{
    public void Bar(IEnumerable x);
}

API sau khi thay đổi:

public class Foo
{
    public void Bar(IEnumerable x);
    public void Bar(ICloneable x);
}

Mã khách hàng mẫu làm việc trước khi thay đổi và bị hỏng sau đó:

new Foo().Bar(new int[0]);

Thêm quá tải toán tử chuyển đổi ngầm định mới

Loại: phá vỡ cấp nguồn.

Ngôn ngữ bị ảnh hưởng: C #, VB

Ngôn ngữ không bị ảnh hưởng: F #

API trước khi thay đổi:

public class Foo
{
    public static implicit operator int ();
}

API sau khi thay đổi:

public class Foo
{
    public static implicit operator int ();
    public static implicit operator float ();
}

Mã khách hàng mẫu làm việc trước khi thay đổi và bị hỏng sau đó:

void Bar(int x);
void Bar(float x);
Bar(new Foo());

Lưu ý: F # không bị hỏng, vì nó không có bất kỳ mức hỗ trợ ngôn ngữ nào cho các toán tử bị quá tải, không rõ ràng cũng không ẩn - cả hai phải được gọi trực tiếp là op_Explicitop_Implicit các phương thức.

Thêm phương thức mới

Loại: thay đổi ngữ nghĩa yên tĩnh cấp nguồn.

Ngôn ngữ bị ảnh hưởng: C #, VB

Ngôn ngữ không bị ảnh hưởng: F #

API trước khi thay đổi:

public class Foo
{
}

API sau khi thay đổi:

public class Foo
{
    public void Bar();
}

Mã khách hàng mẫu chịu một thay đổi ngữ nghĩa yên tĩnh:

public static class FooExtensions
{
    public void Bar(this Foo foo);
}

new Foo().Bar();

Lưu ý: F # không bị hỏng, vì nó không hỗ trợ mức ngôn ngữ ExtensionMethodAttributevà yêu cầu các phương thức mở rộng CLS được gọi là phương thức tĩnh.


Chắc chắn Microsoft đã bao gồm điều này ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey

1
@Robert: liên kết của bạn là về một cái gì đó rất khác biệt - nó mô tả các thay đổi vi phạm cụ thể trong chính .NET Framework . Đây là một câu hỏi rộng hơn mô tả các mẫu chung có thể giới thiệu các thay đổi vi phạm trong API của riêng bạn (với tư cách là một tác giả thư viện / khung). Tôi không biết bất kỳ tài liệu nào như vậy từ MS sẽ được hoàn thành, mặc dù mọi liên kết đến như vậy, ngay cả khi những tài liệu không đầy đủ, chắc chắn đều được chào đón.
Pavel Minaev

Trong bất kỳ danh mục "phá vỡ" nào, có vấn đề nào sẽ chỉ trở nên rõ ràng khi chạy không?
Rohit

1
Có, loại "phá vỡ nhị phân". Trong trường hợp đó, bạn đã có một hội đồng bên thứ ba được biên dịch so với tất cả các phiên bản lắp ráp của bạn. Nếu bạn thả phiên bản mới của lắp ráp tại chỗ, lắp ráp bên thứ ba sẽ ngừng hoạt động - đơn giản là nó không tải vào thời gian chạy hoặc nó hoạt động không chính xác.
Pavel Minaev

3
Tôi sẽ thêm những người trong bài viết và bình luận blog.msdn.com/b/ericlippert/archive/2012/01/09/ ích
Lukasz Madon

Câu trả lời:


42

Thay đổi chữ ký phương thức

Loại: Phá vỡ cấp nhị phân

Ngôn ngữ bị ảnh hưởng: C # (VB và F # rất có thể, nhưng chưa được kiểm tra)

API trước khi thay đổi

public static class Foo
{
    public static void bar(int i);
}

API sau khi thay đổi

public static class Foo
{
    public static bool bar(int i);
}

Mã khách hàng mẫu làm việc trước khi thay đổi

Foo.bar(13);

15
Trên thực tế, nó cũng có thể là một sự phá vỡ mức nguồn, nếu ai đó cố gắng tạo một đại biểu cho bar.
Pavel Minaev

Đó cũng là sự thật. Tôi thấy vấn đề đặc biệt này khi tôi thực hiện một số thay đổi đối với các tiện ích in trong ứng dụng công ty của mình. Khi bản cập nhật được phát hành, không phải tất cả các DLL tham chiếu các tiện ích thi được biên dịch lại và phát hành để nó đưa ra một ngoại lệ phương thức.
Justin Drury

1
Điều này quay trở lại thực tế là các kiểu trả về không được tính cho chữ ký của phương thức. Bạn không thể quá tải hai chức năng chỉ dựa trên loại trả về. Vấn đề tương tự.
Jason Short

1
subqestion cho câu trả lời này: có ai biết hàm ý của việc thêm một thanh giá trị mặc định công khai dotnet4 (int i = 0); ' hoặc thay đổi giá trị mặc định từ giá trị này sang giá trị khác?
k3b

1
Đối với những người sẽ hạ cánh trên trang này, tôi nghĩ rằng đối với C # (và "tôi nghĩ" hầu hết các ngôn ngữ OOP khác), Kiểu trả về không đóng góp cho chữ ký phương thức. , câu trả lời là đúng rằng các thay đổi Chữ ký góp phần thay đổi cấp độ nhị phân. NHƯNG ví dụ có vẻ không đúng IMHO ví dụ chính xác mà tôi có thể nghĩ là TRƯỚC số thập phân công cộng Sum (int a, int b) Sau số thập phân công cộng (số thập phân a, số thập phân b) Vui lòng tham khảo liên kết MSDN này 3.6 Chữ ký và quá tải
Bhanu Chhabra

40

Thêm một tham số với giá trị mặc định.

Loại nghỉ: Phá vỡ cấp nhị phân

Ngay cả khi mã nguồn gọi không cần thay đổi, nó vẫn cần được biên dịch lại (giống như khi thêm một tham số thông thường).

Đó là bởi vì C # biên dịch các giá trị mặc định của các tham số trực tiếp vào cụm gọi. Điều đó có nghĩa là nếu bạn không biên dịch lại, bạn sẽ nhận được một MissingMethodException vì hội đồng cũ cố gắng gọi một phương thức với ít đối số hơn.

API trước khi thay đổi

public void Foo(int a) { }

API sau khi thay đổi

public void Foo(int a, string b = null) { }

Mã khách hàng mẫu bị hỏng sau đó

Foo(5);

Mã máy khách cần được biên dịch lại Foo(5, null)ở cấp mã byte. Việc lắp ráp được gọi sẽ chỉ chứa Foo(int, string), không Foo(int). Đó là bởi vì các giá trị tham số mặc định hoàn toàn là một tính năng ngôn ngữ, thời gian chạy .Net không biết gì về chúng. (Điều này cũng giải thích tại sao các giá trị mặc định phải là hằng số thời gian biên dịch trong C #).


2
đây là một thay đổi đột phá ngay cả đối với cấp mã nguồn: Func<int> f = Foo;// điều này sẽ thất bại với chữ ký đã thay đổi
Vagaus

26

Điều này rất không rõ ràng khi tôi phát hiện ra nó, đặc biệt là về sự khác biệt với tình huống tương tự cho các giao diện. Nó hoàn toàn không phải là một sự phá vỡ, nhưng thật đáng ngạc nhiên khi tôi quyết định đưa nó vào:

Tái cấu trúc các thành viên của lớp thành một lớp cơ sở

Loại: không nghỉ ngơi!

Ngôn ngữ bị ảnh hưởng: không có (tức là không bị hỏng)

API trước khi thay đổi:

class Foo
{
    public virtual void Bar() {}
    public virtual void Baz() {}
}

API sau khi thay đổi:

class FooBase
{
    public virtual void Bar() {}
}

class Foo : FooBase
{
    public virtual void Baz() {}
}

Mã mẫu tiếp tục hoạt động trong suốt thay đổi (mặc dù tôi dự đoán nó sẽ bị hỏng):

// C++/CLI
ref class Derived : Foo
{
   public virtual void Baz() {{

   // Explicit override    
   public virtual void BarOverride() = Foo::Bar {}
};

Ghi chú:

C ++ / CLI là ngôn ngữ .NET duy nhất có cấu trúc thực hiện giao diện tương tự rõ ràng cho các thành viên lớp cơ sở ảo - "ghi đè rõ ràng". Tôi hoàn toàn mong đợi rằng sẽ dẫn đến một loại vỡ như khi di chuyển các thành viên giao diện sang giao diện cơ sở (vì IL được tạo để ghi đè rõ ràng cũng giống như khi thực hiện rõ ràng). Thật ngạc nhiên, đây không phải là trường hợp - mặc dù IL được tạo ra vẫn chỉ định rằng BarOverrideghi đè Foo::Barthay vì FooBase::Bar, trình tải lắp ráp đủ thông minh để thay thế chính xác cho nhau mà không có bất kỳ khiếu nại nào - rõ ràng, thực tế Foolà một lớp là điều tạo ra sự khác biệt. Đi nào ...


3
Miễn là lớp cơ sở là trong cùng một hội đồng. Nếu không, nó là một thay đổi phá vỡ nhị phân.
Jeremy

@Jeremy loại mã nào bị phá vỡ trong trường hợp đó? Bất kỳ người gọi bên ngoài nào sử dụng Baz () sẽ phá vỡ hay đó chỉ là vấn đề với những người cố gắng mở rộng Foo và ghi đè lên Baz ()?
ChaseMedallion

@ChaseMedallion nó sẽ bị hỏng nếu bạn là người dùng cũ. Ví dụ, DLL được biên dịch tham chiếu một phiên bản cũ hơn của Foo và bạn tham chiếu DLL đã biên dịch, nhưng cũng sử dụng phiên bản mới hơn của Foo DLL. Nó bị hỏng với một lỗi lạ, hoặc ít nhất là nó đã xảy ra với tôi trong các thư viện mà tôi đã phát triển trước đó.
Jeremy

19

Đây là một trường hợp đặc biệt không rõ ràng về "thêm / xóa thành viên giao diện", và tôi cho rằng nó xứng đáng với mục nhập riêng của mình trong trường hợp khác mà tôi sẽ đăng tiếp theo. Vì thế:

Tái cấu trúc các thành viên giao diện thành một giao diện cơ sở

Loại: phá vỡ ở cả cấp độ nguồn và nhị phân

Các ngôn ngữ bị ảnh hưởng: C #, VB, C ++ / CLI, F # (đối với ngắt nguồn; nhị phân tự nhiên ảnh hưởng đến bất kỳ ngôn ngữ nào)

API trước khi thay đổi:

interface IFoo
{
    void Bar();
    void Baz();
}

API sau khi thay đổi:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Mã khách hàng mẫu bị hỏng do thay đổi ở cấp nguồn:

class Foo : IFoo
{
   void IFoo.Bar() { ... }
   void IFoo.Baz() { ... }
}

Mã khách hàng mẫu bị phá vỡ bởi sự thay đổi ở cấp nhị phân;

(new Foo()).Bar();

Ghi chú:

Đối với phá vỡ mức nguồn, vấn đề là C #, VB và C ++ / CLI đều yêu cầu chính xác tên giao diện trong khai báo thực hiện thành viên giao diện; do đó, nếu thành viên được chuyển sang giao diện cơ sở, mã sẽ không còn được biên dịch nữa.

Phá vỡ nhị phân là do thực tế là các phương thức giao diện có đủ điều kiện trong IL được tạo để triển khai rõ ràng và tên giao diện cũng phải chính xác.

Việc triển khai ngầm nếu có sẵn (ví dụ C # và C ++ / CLI, nhưng không phải VB) sẽ hoạt động tốt ở cả cấp độ nguồn và nhị phân. Các cuộc gọi phương thức cũng không phá vỡ.


Điều đó không đúng với tất cả các ngôn ngữ. Đối với VB nó không phải là một sự thay đổi mã nguồn. Đối với C # đó là.
Jeremy

Vậy Implements IFoo.Barsẽ minh bạch tham khảo IFooBase.Bar?
Pavel Minaev

Vâng, thực tế, bạn có thể tham chiếu một thành viên trực tiếp hoặc gián tiếp thông qua giao diện kế thừa khi bạn thực hiện nó. Tuy nhiên, đây luôn là một sự thay đổi nhị phân.
Jeremy

15

Sắp xếp lại các giá trị liệt kê

Loại nghỉ: Thay đổi ngữ nghĩa yên tĩnh ở cấp độ nguồn / nhị phân

Ngôn ngữ bị ảnh hưởng: tất cả

Sắp xếp lại các giá trị được liệt kê sẽ giữ khả năng tương thích ở mức nguồn vì các chữ có cùng tên, nhưng các chỉ số thứ tự của chúng sẽ được cập nhật, điều này có thể gây ra một số loại phá vỡ mức nguồn im lặng.

Tệ hơn nữa là các ngắt cấp nhị phân im lặng có thể được giới thiệu nếu mã máy khách không được biên dịch lại so với phiên bản API mới. Các giá trị của Enum là các hằng số thời gian biên dịch và do đó, bất kỳ việc sử dụng nào của chúng đều được đưa vào IL của tổ hợp máy khách. Trường hợp này có thể đặc biệt khó phát hiện ra.

API trước khi thay đổi

public enum Foo
{
   Bar,
   Baz
}

API sau khi thay đổi

public enum Foo
{
   Baz,
   Bar
}

Mã khách hàng mẫu hoạt động nhưng bị hỏng sau đó:

Foo.Bar < Foo.Baz

12

Đây thực sự là một điều rất hiếm trong thực tế, nhưng dù sao nó cũng là một điều đáng ngạc nhiên khi nó xảy ra.

Thêm thành viên mới không quá tải

Loại: phá vỡ mức nguồn hoặc thay đổi ngữ nghĩa yên tĩnh.

Ngôn ngữ bị ảnh hưởng: C #, VB

Ngôn ngữ không bị ảnh hưởng: F #, C ++ / CLI

API trước khi thay đổi:

public class Foo
{
}

API sau khi thay đổi:

public class Foo
{
    public void Frob() {}
}

Mã khách hàng mẫu bị hỏng do thay đổi:

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

Ghi chú:

Vấn đề ở đây là do suy luận kiểu lambda trong C # và VB khi có độ phân giải quá tải. Một hình thức gõ vịt hạn chế được sử dụng ở đây để phá vỡ các mối quan hệ có nhiều loại khớp với nhau, bằng cách kiểm tra xem cơ thể của lambda có hợp lý với một loại nhất định hay không - nếu chỉ một loại dẫn đến cơ thể có thể biên dịch được, thì loại đó được chọn.

Điều nguy hiểm ở đây là mã máy khách có thể có một nhóm phương thức bị quá tải trong đó một số phương thức lấy các đối số của các kiểu của chính nó và các phương thức khác lấy các đối số của các loại được thư viện của bạn đưa ra. Nếu bất kỳ mã nào của anh ta dựa vào thuật toán suy luận kiểu để xác định phương thức đúng chỉ dựa vào sự hiện diện hay vắng mặt của các thành viên, thì việc thêm một thành viên mới vào một trong các loại của bạn có cùng tên như trong một trong các loại của khách hàng có thể có khả năng gây ra suy luận tắt, dẫn đến sự mơ hồ trong quá trình giải quyết quá tải.

Lưu ý rằng các loại FooBartrong ví dụ này không liên quan theo bất kỳ cách nào, không phải do thừa kế cũng không theo cách khác. Chỉ sử dụng chúng trong một nhóm phương thức duy nhất là đủ để kích hoạt điều này và nếu điều này xảy ra trong mã máy khách, bạn không có quyền kiểm soát đối với nó.

Mã mẫu ở trên cho thấy một tình huống đơn giản hơn trong đó đây là ngắt cấp nguồn (tức là kết quả lỗi trình biên dịch). Tuy nhiên, đây cũng có thể là một thay đổi ngữ nghĩa thầm lặng, nếu tình trạng quá tải được chọn thông qua suy luận có các đối số khác sẽ khiến nó được xếp hạng bên dưới (ví dụ: các đối số tùy chọn có giá trị mặc định hoặc nhập không khớp giữa khai báo và đối số thực tế yêu cầu ẩn chuyển đổi). Trong kịch bản như vậy, độ phân giải quá tải sẽ không còn bị lỗi nữa, nhưng một quá tải khác sẽ được trình biên dịch lặng lẽ chọn. Tuy nhiên, trong thực tế, rất khó để chạy vào trường hợp này mà không cẩn thận xây dựng chữ ký phương thức để cố tình gây ra nó.


9

Chuyển đổi một triển khai giao diện ngầm thành một giao diện rõ ràng.

Loại nghỉ: Nguồn và Nhị phân

Ngôn ngữ bị ảnh hưởng: Tất cả

Đây thực sự chỉ là một biến thể của việc thay đổi khả năng truy cập của một phương thức - nó chỉ tinh tế hơn một chút vì dễ dàng bỏ qua thực tế là không phải tất cả quyền truy cập vào các phương thức của giao diện đều nhất thiết phải thông qua tham chiếu đến loại giao diện.

API trước khi thay đổi:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator();
}

API sau khi thay đổi:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator();
}

Mã khách hàng mẫu hoạt động trước khi thay đổi và bị hỏng sau đó:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public

7

Chuyển đổi một triển khai giao diện rõ ràng thành một giao diện ngầm.

Loại nghỉ: Nguồn

Ngôn ngữ bị ảnh hưởng: Tất cả

Việc tái cấu trúc một triển khai giao diện rõ ràng thành một giao diện ngầm sẽ tinh tế hơn trong cách nó có thể phá vỡ một API. Nhìn bề ngoài, có vẻ như điều này sẽ tương đối an toàn, tuy nhiên, khi kết hợp với thừa kế nó có thể gây ra vấn đề.

API trước khi thay đổi:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}

API sau khi thay đổi:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator() { yield return "Foo"; }
}

Mã khách hàng mẫu hoạt động trước khi thay đổi và bị hỏng sau đó:

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"

Xin lỗi, tôi hoàn toàn không tuân theo - chắc chắn mã mẫu trước khi thay đổi API hoàn toàn không biên dịch, vì trước khi thay đổi Fookhông có phương thức công khai được đặt tên GetEnumeratorvà bạn đang gọi phương thức đó qua tham chiếu loại Foo.. .
Pavel Minaev

Thật vậy, tôi đã cố gắng đơn giản hóa một ví dụ từ bộ nhớ và nó đã kết thúc 'foobar' (xin lỗi vì chơi chữ). Tôi đã cập nhật ví dụ để chứng minh chính xác trường hợp (và có thể biên dịch được).
LBushkin

Trong ví dụ của tôi, vấn đề được gây ra bởi không chỉ là sự chuyển đổi của một phương thức giao diện từ ẩn thành công khai. Nó phụ thuộc vào cách trình biên dịch C # xác định phương thức nào sẽ gọi trong vòng lặp foreach. Với các quy tắc độ phân giải của trình biên dịch ses, nó chuyển từ phiên bản trong lớp dẫn xuất sang phiên bản trong lớp cơ sở.
LBushkin

Bạn đã quên yield return "Bar":) nhưng vâng, tôi thấy nơi này sẽ diễn ra ngay bây giờ - foreachluôn gọi phương thức công khai có tên GetEnumerator, ngay cả khi đó không phải là triển khai thực sự cho IEnumerable.GetEnumerator. Điều này dường như có thêm một góc độ: ngay cả khi bạn chỉ có một lớp và nó thực hiện IEnumerablerõ ràng, điều này có nghĩa là đó là một thay đổi đột phá nguồn để thêm một phương thức công khai được đặt tên GetEnumeratorcho nó, bởi vì bây giờ foreachsẽ sử dụng phương thức đó trên triển khai giao diện. Ngoài ra, vấn đề tương tự cũng được áp dụng để IEnumeratorthực hiện ...
Pavel Minaev

6

Thay đổi một lĩnh vực để một tài sản

Loại nghỉ: API

Ngôn ngữ bị ảnh hưởng: Visual Basic và C # *

Thông tin: Khi bạn thay đổi một trường bình thường hoặc biến thành một thuộc tính trong cơ bản trực quan, bất kỳ mã bên ngoài nào tham chiếu thành viên đó theo bất kỳ cách nào sẽ cần phải được biên dịch lại.

API trước khi thay đổi:

Public Class Foo    
    Public Shared Bar As String = ""    
End Class

API sau khi thay đổi:

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    

Mã khách hàng mẫu hoạt động nhưng bị hỏng sau đó:

Foo.Bar = "foobar"

2
Nó thực sự cũng sẽ phá vỡ mọi thứ trong C #, bởi vì các thuộc tính không thể được sử dụng outrefđối số của các phương thức, không giống như các trường và không thể là mục tiêu của toán tử đơn nguyên &.
Pavel Minaev

5

Bổ sung không gian tên

Ngắt cấp độ nguồn / Thay đổi ngữ nghĩa yên tĩnh ở cấp nguồn

Do cách phân giải không gian tên hoạt động trong vb.Net, việc thêm một không gian tên vào thư viện có thể khiến mã Visual Basic được biên dịch với phiên bản API trước đó không biên dịch được với phiên bản mới.

Mã khách hàng mẫu:

Imports System
Imports Api.SomeNamespace

Public Class Foo
    Public Sub Bar()
        Dim dr As Data.DataRow
    End Sub
End Class

Nếu một phiên bản mới của API thêm không gian tên Api.SomeNamespace.Data , thì đoạn mã trên sẽ không biên dịch.

Nó trở nên phức tạp hơn với nhập khẩu không gian tên cấp dự án. Nếu Imports Systemđược bỏ qua từ đoạn mã trên, nhưngSystem không gian tên được nhập ở cấp dự án, thì mã vẫn có thể dẫn đến lỗi.

Tuy nhiên, nếu Api bao gồm một lớp DataRow trong Api.SomeNamespace.Datakhông gian tên của nó , thì mã sẽ biên dịch nhưng drsẽ là một thể hiện System.Data.DataRowkhi được biên dịch với phiên bản cũ của API vàApi.SomeNamespace.Data.DataRow khi được biên dịch với phiên bản API mới.

Đổi tên đối số

Phá vỡ mức nguồn

Thay đổi tên của các đối số là một thay đổi đột phá trong vb.net từ phiên bản 7 (?) (.Net phiên bản 1?) Và c # .net từ phiên bản 4 (.Net phiên bản 4).

API trước khi thay đổi:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API sau khi thay đổi:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string y) {
           ...
        }
    }
}

Mã khách hàng mẫu:

Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB

Tham số tham chiếu

Phá vỡ mức nguồn

Thêm ghi đè phương thức có cùng chữ ký ngoại trừ một tham số được truyền bằng tham chiếu thay vì theo giá trị sẽ khiến nguồn vb tham chiếu API không thể giải quyết hàm. Visual Basic không có cách nào (?) Để phân biệt các phương thức này tại điểm gọi trừ khi chúng có các tên đối số khác nhau, do đó, một thay đổi như vậy có thể khiến cả hai thành viên không thể sử dụng được từ mã vb.

API trước khi thay đổi:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API sau khi thay đổi:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
        public static void Bar(ref string x) {
           ...
        }
    }
}

Mã khách hàng mẫu:

Api.SomeNamespace.Foo.Bar(str)

Trường thay đổi tài sản

Ngắt cấp nhị phân / Ngắt cấp nguồn

Bên cạnh sự phá vỡ mức nhị phân rõ ràng, điều này có thể gây ra sự phá vỡ mức nguồn nếu thành viên được chuyển đến một phương thức bằng cách tham chiếu.

API trước khi thay đổi:

namespace SomeNamespace {
    public class Foo {
        public int Bar;
    }
}

API sau khi thay đổi:

namespace SomeNamespace {
    public class Foo {
        public int Bar { get; set; }
    }
}

Mã khách hàng mẫu:

FooBar(ref Api.SomeNamespace.Foo.Bar);

4

Thay đổi API:

  1. Thêm thuộc tính [Lỗi thời] (bạn thường đề cập đến thuộc tính này bằng cách đề cập đến các thuộc tính; tuy nhiên, đây có thể là một thay đổi đột phá khi sử dụng cảnh báo là lỗi.)

Phá vỡ cấp nhị phân:

  1. Di chuyển một loại từ lắp ráp này sang lắp ráp khác
  2. Thay đổi không gian tên của một loại
  3. Thêm một loại lớp cơ sở từ một hội đồng khác.
  4. Thêm một thành viên mới (được bảo vệ sự kiện) sử dụng một loại từ một tổ hợp khác (Class2) làm ràng buộc đối số mẫu.

    protected void Something<T>() where T : Class2 { }
  5. Thay đổi một lớp con (Class3) để xuất phát từ một kiểu trong một tổ hợp khác khi lớp được sử dụng làm đối số mẫu cho lớp này.

    protected class Class3 : Class2 { }
    protected void Something<T>() where T : Class3 { }

Thay đổi ngữ nghĩa yên tĩnh cấp nguồn:

  1. Thêm / xóa / thay đổi ghi đè của Equals (), GetHashCode () hoặc ToString ()

(không chắc những nơi này phù hợp)

Thay đổi triển khai:

  1. Thêm / xóa phụ thuộc / tham chiếu
  2. Cập nhật phụ thuộc lên phiên bản mới hơn
  3. Thay đổi 'nền tảng đích' giữa x86, Itanium, x64 hoặc anycpu
  4. Xây dựng / thử nghiệm trên một cài đặt khung khác nhau (nghĩa là cài đặt 3.5 trên hộp .Net 2.0 cho phép các lệnh gọi API yêu cầu .Net 2.0 SP2)

Bootstrap / Thay đổi cấu hình:

  1. Thêm / xóa / thay đổi tùy chọn cấu hình tùy chỉnh (ví dụ: cài đặt App.config)
  2. Với việc sử dụng nhiều IoC / DI trong các ứng dụng ngày nay, đôi khi cần phải cấu hình lại và / hoặc thay đổi mã bootstrapping cho mã phụ thuộc DI.

Cập nhật:

Xin lỗi, tôi đã không nhận ra rằng lý do duy nhất khiến điều này bị phá vỡ đối với tôi là tôi đã sử dụng chúng trong các ràng buộc mẫu.


"Thêm một thành viên mới (được bảo vệ sự kiện) sử dụng một loại từ hội đồng khác." - IIRC, khách hàng chỉ cần tham chiếu các tập hợp phụ thuộc có chứa các kiểu cơ sở của tập hợp mà nó đã tham chiếu; nó không phải tham chiếu các hội đồng chỉ được sử dụng (ngay cả khi các kiểu trong chữ ký phương thức); Tôi không chắc chắn 100% về điều này. Bạn có một tài liệu tham khảo cho các quy tắc chính xác cho điều này? Ngoài ra, di chuyển một loại có thể không phá vỡ nếu TypeForwardedToAttributeđược sử dụng.
Pavel Minaev

"TypeForwardedTo" là tin tức với tôi, tôi sẽ kiểm tra nó. Về phần khác, tôi cũng không 100% về điều đó ... hãy để tôi xem có thể repro không và tôi sẽ cập nhật bài viết.
csharptest.net

Vì vậy, sau đó, không bắt buộc -Werrortrong hệ thống xây dựng của bạn mà bạn gửi với tarball phát hành. Cờ này hữu ích nhất cho nhà phát triển mã và thường không có ích cho người tiêu dùng.
biley

@binki điểm tuyệt vời, coi các cảnh báo là lỗi chỉ nên có trong các bản dựng DEBUG.
csharptest.net

3

Thêm phương thức quá tải để hủy bỏ việc sử dụng tham số mặc định

Loại nghỉ: Thay đổi ngữ nghĩa yên tĩnh cấp nguồn

Bởi vì trình biên dịch chuyển đổi các cuộc gọi phương thức với các giá trị tham số mặc định bị thiếu thành một cuộc gọi rõ ràng với giá trị mặc định ở phía gọi, tính tương thích cho mã được biên dịch hiện có được đưa ra; một phương thức có chữ ký chính xác sẽ được tìm thấy cho tất cả các mã được biên dịch trước đó.

Mặt khác, các cuộc gọi không sử dụng các tham số tùy chọn hiện được biên dịch thành một cuộc gọi đến phương thức mới thiếu tham số tùy chọn. Tất cả vẫn hoạt động tốt, nhưng nếu mã được gọi nằm trong một cụm khác, mã được biên dịch mới gọi thì bây giờ phụ thuộc vào phiên bản mới của lắp ráp này. Việc triển khai các hội đồng gọi mã tái cấu trúc mà không triển khai lắp ráp mã được tái cấu trúc nằm trong đó dẫn đến các ngoại lệ "không tìm thấy phương thức".

API trước khi thay đổi

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
  {
     return mandatoryParameter + optionalParameter;
  }    

API sau khi thay đổi

  public int MyMethod(int mandatoryParameter, int optionalParameter)
  {
     return mandatoryParameter + optionalParameter;
  }

  public int MyMethod(int mandatoryParameter)
  {
     return MyMethod(mandatoryParameter, 0);
  }

Mã mẫu vẫn sẽ hoạt động

  public int CodeNotDependentToNewVersion()
  {
     return MyMethod(5, 6); 
  }

Mã mẫu hiện phụ thuộc vào phiên bản mới khi biên dịch

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }

1

Đổi tên một giao diện

Kinda of Break: Nguồn và nhị phân

Ngôn ngữ bị ảnh hưởng: Rất có thể là tất cả, được thử nghiệm trong C #.

API trước khi thay đổi:

public interface IFoo
{
    void Test();
}

public class Bar
{
    IFoo GetFoo() { return new Foo(); }
}

API sau khi thay đổi:

public interface IFooNew // Of the exact same definition as the (old) IFoo
{
    void Test();
}

public class Bar
{
    IFooNew GetFoo() { return new Foo(); }
}

Mã khách hàng mẫu hoạt động nhưng bị hỏng sau đó:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break

1

Phương thức quá tải với một tham số loại nullable

Loại: Phá vỡ cấp nguồn

Ngôn ngữ bị ảnh hưởng: C #, VB

API trước khi thay đổi:

public class Foo
{
    public void Bar(string param);
}

API sau khi thay đổi:

public class Foo
{
    public void Bar(string param);
    public void Bar(int? param);
}

Mã khách hàng mẫu làm việc trước khi thay đổi và bị hỏng sau đó:

new Foo().Bar(null);

Ngoại lệ: Cuộc gọi không rõ ràng giữa các phương thức hoặc thuộc tính sau.


0

Khuyến mãi cho một phương thức mở rộng

Loại: phá vỡ mức nguồn

Ngôn ngữ bị ảnh hưởng: C # v6 trở lên (có thể là ngôn ngữ khác?)

API trước khi thay đổi:

public static class Foo
{
    public static void Bar(string x);
}

API sau khi thay đổi:

public static class Foo
{
    public void Bar(this string x);
}

Mã khách hàng mẫu làm việc trước khi thay đổi và bị hỏng sau đó:

using static Foo;

class Program
{
    static void Main() => Bar("hello");
}

Thông tin thêm: https://github.com/dotnet/csharplang/issues/665

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.