คำแนะนำที่ชัดเจนเกี่ยวกับการเปลี่ยนแปลงการใช้ API ใน. NET


227

ฉันต้องการรวบรวมข้อมูลให้มากที่สุดเท่าที่จะเป็นไปได้เกี่ยวกับการกำหนดเวอร์ชัน API ใน. NET / CLR และโดยเฉพาะการเปลี่ยนแปลง API ที่ทำหรือไม่ทำลายแอปพลิเคชันไคลเอนต์ ก่อนอื่นมานิยามคำศัพท์กัน:

การเปลี่ยนแปลง API - การเปลี่ยนแปลงคำจำกัดความที่มองเห็นแบบสาธารณะรวมถึงสมาชิกสาธารณะใด ๆ ซึ่งรวมถึงการเปลี่ยนประเภทและชื่อสมาชิกการเปลี่ยนประเภทพื้นฐานการเพิ่ม / การลบอินเตอร์เฟสจากรายการอินเตอร์เฟสที่ใช้งานการเพิ่ม / ลบสมาชิก (รวมถึงการโอเวอร์โหลด) การเปลี่ยนการมองเห็นสมาชิกวิธีการเปลี่ยนชื่อและพารามิเตอร์ชนิดการเพิ่มค่าเริ่มต้น สำหรับพารามิเตอร์เมธอดการเพิ่ม / ลบแอ็ตทริบิวต์กับชนิดและสมาชิกและการเพิ่ม / ลบพารามิเตอร์ประเภททั่วไปบนประเภทและสมาชิก (ฉันพลาดอะไรเลยหรือ?) สิ่งนี้ไม่รวมถึงการเปลี่ยนแปลงใด ๆ ในเนื้อความของสมาชิกหรือการเปลี่ยนแปลงใด ๆ กับสมาชิกส่วนตัว (เช่นเราไม่คำนึงถึงการสะท้อนกลับ)

ตัวแบ่งระดับไบนารี - การเปลี่ยนแปลง API ที่ส่งผลให้แอสเซมบลีไคลเอนต์ที่คอมไพล์เทียบกับเวอร์ชันเก่ากว่าของ API อาจไม่โหลดด้วยเวอร์ชันใหม่ ตัวอย่าง: การเปลี่ยนลายเซ็นเมธอดแม้ว่าจะอนุญาตให้เรียกในลักษณะเดียวกับก่อนหน้า (เช่น: void เพื่อส่งคืนค่าเริ่มต้นของประเภท / พารามิเตอร์เกินพิกัด)

ตัวแบ่งระดับซอร์ส - การเปลี่ยนแปลง API ที่ส่งผลให้โค้ดที่มีอยู่ถูกเขียนเพื่อคอมไพล์เทียบกับเวอร์ชันเก่ากว่าของ API ที่อาจไม่คอมไพล์ด้วยเวอร์ชันใหม่ แอสเซมบลีไคลเอนต์ที่คอมไพล์แล้วทำงานเหมือน แต่ก่อน ตัวอย่าง: การเพิ่มโอเวอร์โหลดใหม่ที่อาจทำให้เกิดความกำกวมในการเรียกใช้เมธอดที่ไม่ชัดเจนก่อนหน้านี้

การเปลี่ยนแปลงซีแมนทิกส์ระดับซอร์สนั้นเป็นการเปลี่ยนแปลง API ซึ่งส่งผลให้โค้ดที่มีอยู่เขียนขึ้นเพื่อคอมไพล์กับเวอร์ชันเก่าของ API เปลี่ยนซีแมนทิกส์อย่างเงียบ ๆ เช่นโดยเรียกวิธีการอื่น รหัสควรรวบรวมต่อไปโดยไม่มีคำเตือน / ข้อผิดพลาดและแอสเซมบลีที่รวบรวมไว้ก่อนหน้านี้ควรทำงานเหมือนก่อน ตัวอย่าง: การใช้อินเทอร์เฟซใหม่บนคลาสที่มีอยู่ซึ่งส่งผลให้มีการเลือกเกินพิกัดที่แตกต่างกันระหว่างการแก้ปัญหาโอเวอร์โหลด

เป้าหมายสูงสุดคือการจัดหมวดหมู่การเปลี่ยนแปลง API ของซีแมนทิกส์ที่เงียบและสงบที่สุดให้มากที่สุดเท่าที่จะทำได้และอธิบายถึงผลกระทบที่แท้จริงของการแตกหักและภาษาใดบ้างที่ไม่ได้รับผลกระทบ ในการขยายในภายหลัง: ในขณะที่การเปลี่ยนแปลงบางอย่างส่งผลกระทบต่อทุกภาษาในระดับสากล (เช่นการเพิ่มสมาชิกใหม่ไปยังอินเทอร์เฟซจะทำลายการใช้งานของอินเทอร์เฟซนั้นในภาษาใด ๆ ) บางคนต้องการความหมายภาษาเฉพาะอย่างมาก โดยทั่วไปมักเกี่ยวข้องกับวิธีการมากไปและโดยทั่วไปแล้วสิ่งที่เกี่ยวข้องกับการแปลงประเภทโดยนัย ดูเหมือนจะไม่มีทางใดที่จะกำหนด "ตัวหารร่วมที่น้อยที่สุด" ที่นี่แม้สำหรับภาษาที่สอดคล้องกับ CLS (นั่นคือภาษาที่สอดคล้องกับกฎของ "ผู้บริโภค CLS" อย่างน้อยตามที่กำหนดไว้ในข้อกำหนด CLI) - แม้ว่าฉันจะ จะขอบคุณถ้ามีคนแก้ไขฉันว่าทำผิดที่นี่ - นี่จะต้องใช้ภาษาเป็นภาษา สิ่งที่น่าสนใจที่สุดคือสิ่งที่มาพร้อมกับ. NET แบบนอกกรอบ: C #, VB และ F #; แต่คนอื่น ๆ เช่น IronPython, IronRuby, Delphi Prism เป็นต้นก็มีความเกี่ยวข้องเช่นกัน ยิ่งมีมุมมากเท่าไหร่ก็ยิ่งมีความน่าสนใจมากขึ้นเท่านั้น - สิ่งต่าง ๆ เช่นการลบสมาชิกนั้นค่อนข้างชัดเจนในตัวเอง แต่การโต้ตอบที่ลึกซึ้งระหว่างวิธีการโอเวอร์โหลดวิธีการพารามิเตอร์ทางเลือก / ค่าเริ่มต้นการอนุมานประเภทแลมบ์ดา ในช่วงเวลาที่.

ตัวอย่างเล็ก ๆ น้อย ๆ ที่จะเริ่มต้นนี้:

การเพิ่มเมธอด overloads ใหม่

ชนิด: ตัวแบ่งระดับซอร์ส

ภาษาที่ได้รับผลกระทบ: 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 ที่จะเรียกว่าเป็นวิธีการคงที่


แน่นอนว่า Microsoft ครอบคลุมสิ่งนี้อยู่แล้ว ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey

1
@ Robert: ลิงค์ของคุณเกี่ยวกับสิ่งที่แตกต่างกันมาก - มันอธิบายการเปลี่ยนแปลงการทำลายที่เฉพาะเจาะจงใน. NET Frameworkเอง นี่เป็นคำถามที่กว้างขึ้นที่อธิบายรูปแบบทั่วไปที่สามารถนำมาซึ่งการเปลี่ยนแปลงที่สำคัญในAPI ของคุณเอง (ในฐานะผู้เขียนไลบรารี / กรอบงาน) ฉันไม่ได้ตระหนักถึงเอกสารดังกล่าวจาก MS ที่จะเสร็จสมบูรณ์แม้ว่าจะมีการเชื่อมโยงไปยังเอกสารดังกล่าวแม้ว่าจะไม่สมบูรณ์ก็ตามก็ตาม
Pavel Minaev

ในหมวดหมู่ "หยุดพัก" เหล่านี้มีปัญหาใดบ้างที่จะปรากฏให้เห็นขณะรันไทม์?
Rohit

1
ใช่หมวดหมู่ "ตัวแบ่งไบนารี" ในกรณีดังกล่าวคุณได้รวบรวมแอสเซมบลีของบุคคลที่สามไว้แล้วกับแอสเซมบลีของคุณทุกรุ่น ถ้าคุณปล่อยแอสเซมบลีของคุณรุ่นใหม่แทนที่แอสเซมบลีของ บริษัท อื่นหยุดทำงาน - ไม่สามารถโหลดได้ในเวลาใช้งานหรือทำงานไม่ถูกต้อง
Pavel Minaev

3
ฉันจะเพิ่มผู้ที่อยู่ในโพสต์และแสดงความคิดเห็นblogs.msdn.com/b/ericlippert/archive/2012/01/09/ …
Lukasz Madon

คำตอบ:


42

การเปลี่ยนลายเซ็นวิธี

ชนิด: ตัวแบ่งระดับไบนารี

ภาษาที่ได้รับผลกระทบ: 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);

15
barในความเป็นจริงมันอาจจะเป็นแหล่งที่มาของการแบ่งระดับเกินไปถ้ามีคนพยายามที่จะสร้างผู้รับมอบสิทธิ์สำหรับ
Pavel Minaev

ก็จริงเช่นกัน ฉันพบปัญหานี้โดยเฉพาะเมื่อฉันทำการเปลี่ยนแปลงบางอย่างกับยูทิลิตี้การพิมพ์ในแอปพลิเคชัน บริษัท ของฉัน เมื่อมีการเปิดตัวการปรับปรุงไม่ใช่ DLLs ทั้งหมดที่อ้างถึงยูทิลิตี thi ที่อ้างถึงนั้นได้ทำการคอมไพล์ใหม่และปล่อยออกมา
Justin Drury

1
สิ่งนี้จะกลับไปสู่ความจริงที่ว่าประเภทการคืนสินค้าจะไม่นับรวมกับลายเซ็นของวิธีการ คุณไม่สามารถโหลดเกินสองฟังก์ชั่นตามประเภทผลตอบแทนอย่างเดียว ปัญหาเดียวกัน.
46432 Jason สั้นสั้น

1
คำถามย่อยสำหรับคำตอบนี้: ไม่มีใครทราบถึงความหมายของการเพิ่ม dotnet4 defaultvalue 'public void bar (int i = 0);' หรือการเปลี่ยนค่าเริ่มต้นนั้นจากค่าหนึ่งไปเป็นอีกค่าหนึ่ง?
k3b

1
สำหรับผู้ที่กำลังจะลงจอดบนหน้านี้ ฉันคิดว่าสำหรับ C # (และ "ฉันคิดว่า" ภาษา OOP อื่น ๆ ส่วนใหญ่) ประเภทการคืนไม่ได้มีส่วนร่วมในการลงนามวิธีการ ใช่คำตอบนั้นถูกต้องที่การเปลี่ยนแปลงของลายเซ็นมีส่วนทำให้เกิดการเปลี่ยนแปลงระดับไบนารี แต่ตัวอย่างที่ไม่ได้ดูเหมือน IMHO ที่ถูกต้องเป็นตัวอย่างที่ถูกต้องที่ฉันสามารถคิดเป็น ก่อนทศนิยมสาธารณะซำ (int A, B int) หลังจาก ทศนิยมสาธารณะซำ (ทศนิยมทศนิยมข) กรุณาดูที่ลิงค์นี้ MSDN 3.6 ลายเซ็นและการบรรทุกเกินพิกัด
Bhanu Chhabra

40

การเพิ่มพารามิเตอร์ด้วยค่าเริ่มต้น

Kind of Break: Break Binary-level

แม้ว่าซอร์สโค้ดการโทรไม่จำเป็นต้องเปลี่ยนแปลง แต่ก็ยังจำเป็นต้องทำการคอมไพล์ใหม่ (เช่นเดียวกับเมื่อเพิ่มพารามิเตอร์ปกติ)

นั่นเป็นเพราะ C # รวบรวมค่าเริ่มต้นของพารามิเตอร์ลงในแอสเซมบลีการโทรโดยตรง หมายความว่าถ้าคุณไม่คอมไพล์อีกครั้งคุณจะได้รับ MissingMethodException เนื่องจากแอสเซมบลีเก่าพยายามเรียกเมธอดที่มีอาร์กิวเมนต์น้อยลง

API ก่อนการเปลี่ยนแปลง

public void Foo(int a) { }

API หลังจากการเปลี่ยนแปลง

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

ตัวอย่างรหัสลูกค้าที่เสียหลังจากนั้น

Foo(5);

รหัสไคลเอนต์จะต้อง recompiled เป็นFoo(5, null)ในระดับ bytecode ที่เรียกว่าการชุมนุมจะมีเฉพาะไม่Foo(int, string) Foo(int)นั่นเป็นเพราะค่าพารามิเตอร์เริ่มต้นเป็นคุณสมบัติทางภาษาล้วนๆรันไทม์. Net ไม่ทราบอะไรเกี่ยวกับพวกเขา (สิ่งนี้อธิบายได้ด้วยว่าทำไมค่าเริ่มต้นจึงต้องเป็นค่าคงที่เวลาคอมไพล์ใน C #)


2
นี่คือการเปลี่ยนแปลงที่Func<int> f = Foo;ไม่
หยุดยั้ง

26

อันนี้ไม่ชัดเจนมากเมื่อฉันค้นพบมันโดยเฉพาะอย่างยิ่งในแง่ของความแตกต่างกับสถานการณ์เดียวกันสำหรับอินเทอร์เฟซ มันไม่ใช่การหยุดพักเลย แต่ก็น่าแปลกใจที่ฉันตัดสินใจรวมมันไว้ด้วย:

Refactoring สมาชิกคลาสลงในคลาสฐาน

ชนิด: ไม่หยุดพัก!

ภาษาที่ได้รับผลกระทบ: ไม่มี (เช่นไม่มีถูกทำลาย)

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 เพียงภาษาเดียวที่มีการสร้างอินเทอร์เฟซแบบอะนาล็อกเพื่อใช้งานอินเทอร์เฟซที่ชัดเจนสำหรับสมาชิกคลาสฐานเสมือน - "override ชัดเจน" ฉันคาดหวังอย่างเต็มที่ว่าจะส่งผลให้เกิดการแตกหักแบบเดียวกับเมื่อย้ายสมาชิกอินเตอร์เฟสไปยังอินเตอร์เฟสพื้นฐาน (เนื่องจาก IL ที่สร้างขึ้นสำหรับการลบล้างที่ชัดเจนนั้นเหมือนกับการใช้งานอย่างชัดเจน) ที่แปลกใจของฉันนี้เป็นกรณีที่ไม่ - แม้ว่าสร้าง IL ยังระบุว่าBarOverrideแทนที่Foo::Barมากกว่าFooBase::Barการประกอบรถตักดินเป็นพอสมาร์ทเพื่อทดแทนอีกหนึ่งอย่างถูกต้องโดยไม่ต้องร้องเรียนใด ๆ - เห็นได้ชัดว่าความจริงที่ว่าFooเป็นชั้นเรียนเป็นสิ่งที่ทำให้แตกต่าง ไปคิด ...


3
ตราบใดที่คลาสพื้นฐานอยู่ในแอสเซมบลีเดียวกัน มิฉะนั้นจะเป็นการเปลี่ยนแปลงที่ผิดพลาดแบบไบนารี
Jeremy

@ Jeremy มีรหัสประเภทใดบ้างในกรณีนั้น การใช้ Baz () ของผู้โทรเข้าภายนอกจะทำให้เกิดปัญหาหรือเป็นปัญหากับผู้ที่พยายามขยาย Foo และแทนที่ Baz () หรือไม่
ChaseMedallion

@ChaseMedallion มันจะแตกถ้าคุณเป็นผู้ใช้มือสอง ตัวอย่างเช่นคอมไพล์ DLL อ้างอิง Foo เวอร์ชันเก่ากว่าและคุณอ้างอิง DLL ที่คอมไพล์แล้ว แต่ยังใช้ Foo DLL เวอร์ชันที่ใหม่กว่า มันผิดพลาดแปลก ๆ หรืออย่างน้อยก็ทำเพื่อฉันในห้องสมุดที่ฉันเคยพัฒนามาก่อน
เจเรมี

19

กรณีนี้อาจเป็นกรณีพิเศษที่ไม่ชัดเจนนักของ "การเพิ่ม / การลบสมาชิกส่วนต่อประสาน" และฉันคิดว่ามันสมควรที่จะได้รับข้อมูลของตัวเองในกรณีอื่นซึ่งฉันจะโพสต์ถัดไป ดังนั้น:

การเปลี่ยนโครงสร้างสมาชิกอินเตอร์เฟสให้เป็นอินเตอร์เฟสพื้นฐาน

ชนิด: แบ่งที่ทั้งระดับต้นทางและไบนารี

ภาษาที่ได้รับผลกระทบ: 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) จะทำงานได้ดีทั้งในระดับต้นทางและไบนารี วิธีการโทรไม่แตก


นั่นไม่เป็นความจริงสำหรับทุกภาษา สำหรับ VB มันไม่ใช่การเปลี่ยนแปลงรหัสที่มาทำลาย สำหรับ C # มันคือ
Jeremy

ดังนั้นImplements IFoo.Barจะอ้างอิงอย่างโปร่งใสIFooBase.Barหรือไม่
Pavel Minaev

ใช่มันทำจริงคุณสามารถอ้างอิงสมาชิกโดยตรงหรือโดยอ้อมผ่านทางอินเทอร์เฟซการสืบทอดเมื่อคุณใช้งาน อย่างไรก็ตามนี่เป็นการเปลี่ยนแปลงไบนารีเสมอ
Jeremy

15

การจัดเรียงค่าที่แจกแจงใหม่

ชนิดของการหยุดพัก: การเปลี่ยนแปลงความหมายของระดับเงียบ / ระดับแหล่งที่มา / ไบนารี

ภาษาที่ได้รับผลกระทบ: ทั้งหมด

การจัดเรียงค่าที่แจกแจงใหม่จะทำให้ความเข้ากันได้ของแหล่งข้อมูลเป็นไปตามที่ตัวอักษรมีชื่อเหมือนกัน แต่ดัชนีลำดับของพวกเขาจะได้รับการอัปเดตซึ่งอาจทำให้การแบ่งระดับแหล่งที่มาเงียบ ๆ

ที่แย่ยิ่งกว่านั้นคือตัวแบ่งระดับไบนารีเงียบที่สามารถแนะนำได้หากรหัสลูกค้าไม่ได้คอมไพล์ใหม่เทียบกับเวอร์ชั่น API ใหม่ ค่า Enum เป็นค่าคงที่เวลาคอมไพล์และเช่นการใช้งานใด ๆ ของมันจะถูกอบเข้าใน IL ของแอสเซมบลีไคลเอ็นต์ กรณีนี้อาจเป็นเรื่องยากโดยเฉพาะในบางครั้ง

API ก่อนการเปลี่ยนแปลง

public enum Foo
{
   Bar,
   Baz
}

API หลังจากการเปลี่ยนแปลง

public enum Foo
{
   Baz,
   Bar
}

ตัวอย่างรหัสลูกค้าที่ใช้งานได้ แต่ใช้งานไม่ได้ในภายหลัง:

Foo.Bar < Foo.Baz

12

อันนี้เป็นของหายากจริงๆในทางปฏิบัติ แต่กระนั้นก็เป็นเรื่องที่น่าแปลกใจเมื่อมันเกิดขึ้น

การเพิ่มสมาชิกที่ไม่โอเวอร์โหลดใหม่

ชนิด: ตัวแบ่งระดับแหล่งข้อมูลหรือการเปลี่ยนความหมายที่เงียบสงบ

ภาษาที่ได้รับผลกระทบ: 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ในตัวอย่างนี้ไม่เกี่ยวข้องในทางใดทางหนึ่งไม่ใช่โดยทางมรดกหรืออย่างอื่น การใช้พวกมันในกลุ่มวิธีการเดียวนั้นเพียงพอที่จะทริกเกอร์สิ่งนี้และหากสิ่งนี้เกิดขึ้นในรหัสลูกค้าคุณจะไม่สามารถควบคุมมันได้

โค้ดตัวอย่างด้านบนแสดงให้เห็นถึงสถานการณ์ที่ง่ายขึ้นซึ่งนี่คือตัวแบ่งระดับซอร์ส (เช่นผลลัพธ์ข้อผิดพลาดของคอมไพเลอร์) อย่างไรก็ตามสิ่งนี้อาจเป็นการเปลี่ยนซีแมนทิกส์แบบเงียบหากโอเวอร์โหลดที่ถูกเลือกผ่านการอนุมานมีข้อโต้แย้งอื่น ๆ ซึ่งจะทำให้อยู่ในอันดับด้านล่าง (เช่นอาร์กิวเมนต์ตัวเลือกที่มีค่าเริ่มต้นหรือประเภทไม่ตรงกันระหว่างการประกาศและอาร์กิวเมนต์จริง แปลง) ในสถานการณ์เช่นนี้การแก้ปัญหาการโอเวอร์โหลดจะไม่ล้มเหลวอีกต่อไป แต่คอมไพเลอร์จะเลือกโอเวอร์โหลดที่แตกต่างกันอย่างเงียบ ๆ อย่างไรก็ตามในทางปฏิบัติมันเป็นเรื่องยากมากที่จะพบกับกรณีนี้โดยไม่ต้องสร้างลายเซ็นวิธีการอย่างรอบคอบเพื่อทำให้เกิดปัญหาโดยเจตนา


9

แปลงการประยุกต์ใช้อินเตอร์เฟสแบบ implicit เป็นแบบชัดแจ้ง

Kind of Break: Source และ Binary

ภาษาที่ได้รับผลกระทบ: ทั้งหมด

นี่เป็นเพียงรูปแบบที่เปลี่ยนแปลงการเข้าถึงวิธีการของ - เพียงเล็กน้อยเพิ่มเติมเนื่องจากง่ายต่อการมองข้ามข้อเท็จจริงที่ว่าการเข้าถึงวิธีการของส่วนต่อประสานทั้งหมดไม่จำเป็นต้องผ่านการอ้างอิงถึงชนิดของอินเทอร์เฟซ

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

7

แปลงการประยุกต์ใช้อินเทอร์เฟซที่ชัดเจนเป็นหนึ่งโดยนัย

Kind of Break: Source

ภาษาที่ได้รับผลกระทบ: ทั้งหมด

การเปลี่ยนการนำอินเตอร์เฟสที่ชัดเจนไปใช้งานนั้นจะมีความลึกซึ้งในวิธีที่จะทำให้ 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"

ขออภัยฉันไม่ได้ติดตาม - แน่นอนว่าโค้ดตัวอย่างก่อนการเปลี่ยนแปลง API จะไม่รวบรวมเลยเนื่องจากก่อนหน้าการเปลี่ยนแปลงFooจะไม่มีชื่อวิธีสาธารณะGetEnumeratorและคุณกำลังเรียกใช้วิธีนี้ผ่านการอ้างอิงประเภทFoo.. .
Pavel Minaev

อันที่จริงฉันพยายามทำให้ตัวอย่างจากหน่วยความจำง่ายขึ้นและลงเอยด้วย 'foobar' (ให้อภัยปุน) ฉันอัปเดตตัวอย่างเพื่อแสดงกรณีและปัญหาให้ถูกต้อง (และสามารถคอมไพล์ได้)
LBushkin

ในตัวอย่างของฉันปัญหาเกิดจากการเปลี่ยนวิธีการติดต่อไม่ใช่แค่การเปิดเผยสู่สาธารณะ มันขึ้นอยู่กับวิธีที่คอมไพเลอร์ C # กำหนดวิธีการที่จะเรียกในลูป foreach กำหนดกฎการแก้ปัญหาคอมไพเลอร์ ses มันเปลี่ยนจากรุ่นในชั้นเรียนที่ได้รับไปเป็นรุ่นในชั้นฐาน
LBushkin

คุณลืมyield return "Bar":) แต่ใช่ผมเห็นว่านี้เป็นไปตอนนี้ - foreachมักจะเรียกเมธอดสาธารณะชื่อถึงแม้ว่ามันจะไม่ได้การดำเนินงานที่แท้จริงสำหรับGetEnumerator IEnumerable.GetEnumeratorดูเหมือนว่าจะมีมุมหนึ่งเพิ่มเติม: แม้ว่าคุณจะมีเพียงหนึ่งคลาสและมีการดำเนินการIEnumerableอย่างชัดเจนซึ่งหมายความว่ามันเป็นการเปลี่ยนแปลงที่ผิดเพี้ยนของแหล่งที่มาเพื่อเพิ่มวิธีสาธารณะที่มีชื่อGetEnumeratorเนื่องจากตอนนี้foreachจะใช้วิธีนั้นผ่านการใช้อินเตอร์เฟส นอกจากนี้ยังมีปัญหาเดียวกันคือใช้บังคับกับIEnumeratorการดำเนินงาน ...
พาเวล Minaev

6

การเปลี่ยนฟิลด์เป็นคุณสมบัติ

Kind of Break: 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"

2
จริง ๆ แล้วมันจะแบ่งสิ่งต่าง ๆ ใน C # เช่นกันเนื่องจากคุณสมบัติไม่สามารถใช้สำหรับoutและrefข้อโต้แย้งของวิธีการซึ่งแตกต่างจากเขตข้อมูลและไม่สามารถเป็นเป้าหมายของ&ผู้ประกอบการเอก
Pavel Minaev

5

การเพิ่มเนมสเปซ

ตัวแบ่งระดับแหล่งข้อมูล / การเปลี่ยนแปลงความหมายระดับเงียบของแหล่งข้อมูล

เนื่องจากวิธีแก้ปัญหาเนมสเปซทำงานใน 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

พารามิเตอร์อ้างอิง

ตัวแบ่งระดับแหล่งที่มา

การเพิ่มเมธอดการแทนที่ด้วยลายเซ็นเดียวกันยกเว้นว่าหนึ่งพารามิเตอร์ถูกส่งผ่านโดยการอ้างอิงแทนโดยค่าจะทำให้ซอร์ส 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);

4

การเปลี่ยนแปลง API:

  1. การเพิ่มแอ็ตทริบิวต์ [เลิกใช้] (คุณปิดบังสิ่งนี้ด้วยคุณสมบัติที่กล่าวถึงอย่างไรก็ตามอาจเป็นการเปลี่ยนแปลงที่ไม่สมบูรณ์เมื่อใช้การเตือนแบบข้อผิดพลาด

การหยุดพักระดับไบนารี:

  1. การย้ายประเภทจากชุดประกอบหนึ่งไปอีกชุดหนึ่ง
  2. การเปลี่ยนเนมสเปซของประเภท
  3. การเพิ่มประเภทคลาสพื้นฐานจากชุดประกอบอื่น
  4. การเพิ่มสมาชิกใหม่ (ป้องกันเหตุการณ์) ที่ใช้ชนิดจากชุดประกอบอื่น (Class2) เป็นข้อ จำกัด อาร์กิวเมนต์แม่แบบ

    protected void Something<T>() where T : Class2 { }
  5. การเปลี่ยนคลาสลูก (Class3) เพื่อสืบทอดจากประเภทในชุดประกอบอื่นเมื่อใช้คลาสเป็นอาร์กิวเมนต์เท็มเพลตสำหรับคลาสนี้

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

การเปลี่ยนแปลงความหมายเงียบระดับที่มา:

  1. การเพิ่ม / ลบ / เปลี่ยนการแทนที่ของ Equals (), GetHashCode () หรือ ToString ()

(ไม่แน่ใจว่าแบบไหนดี)

การเปลี่ยนแปลงการปรับใช้:

  1. การเพิ่ม / ลบการอ้างอิง / การอ้างอิง
  2. การอัพเดตการพึ่งพาเวอร์ชันที่ใหม่กว่า
  3. การเปลี่ยน 'แพลตฟอร์มเป้าหมาย' ระหว่าง x86, Itanium, x64 หรือ anycpu
  4. การสร้าง / ทดสอบบนการติดตั้งเฟรมเวิร์กต่าง ๆ (เช่นการติดตั้ง 3.5 บนกล่อง. NET 2.0 อนุญาตให้เรียก API ที่ต้องการ. NET 2.0 SP2)

Bootstrap / การเปลี่ยนแปลงการกำหนดค่า:

  1. การเพิ่ม / ลบ / เปลี่ยนตัวเลือกการกำหนดค่าแบบกำหนดเอง (เช่นการตั้งค่า App.config)
  2. ด้วยการใช้งาน IoC / DI อย่างหนักในแอปพลิเคชั่นในปัจจุบันจึงจำเป็นต้องกำหนดค่าและ / หรือเปลี่ยนรหัส bootstrapping สำหรับรหัส DI ที่เกี่ยวข้อง

ปรับปรุง:

ขออภัยฉันไม่ทราบว่าเหตุผลเดียวที่ทำให้ฉันแตกคือเพราะฉันใช้พวกเขาในข้อ จำกัด ของแม่แบบ


"การเพิ่มสมาชิกใหม่ (ป้องกันเหตุการณ์) ที่ใช้ประเภทจากชุดประกอบอื่น" - IIRC ลูกค้าจะต้องอ้างอิงแอสเซมบลีที่ขึ้นต่อกันซึ่งมีประเภทฐานของแอสเซมบลีที่มันอ้างอิงอยู่แล้ว มันไม่จำเป็นต้องอ้างอิงชุดประกอบที่ใช้เพียง (แม้ว่าประเภทอยู่ในลายเซ็นวิธีการ); ฉันไม่แน่ใจ 100% เกี่ยวกับเรื่องนี้ คุณมีการอ้างอิงสำหรับกฎที่แม่นยำสำหรับสิ่งนี้หรือไม่? นอกจากนี้การย้ายประเภทอาจไม่ทำลายหากTypeForwardedToAttributeใช้
Pavel Minaev

"TypeForwardedTo" นั้นเป็นข่าวสำหรับฉันฉันจะลองดู สำหรับคนอื่น ๆ ฉันไม่ได้ 100% เลยขอให้ฉันดูว่าสามารถทำซ้ำได้หรือไม่และฉันจะอัพเดทโพสต์
csharptest.net

ดังนั้นอย่าบังคับใช้-Werrorใน buildsystem ที่คุณจัดส่งด้วย tarballs ที่วางจำหน่าย การตั้งค่าสถานะนี้มีประโยชน์มากที่สุดสำหรับนักพัฒนาของรหัสและส่วนใหญ่ไม่ช่วยเหลือผู้บริโภค
binki

@binki ดีเยี่ยมจุดปฏิบัติตามคำเตือนเป็นข้อผิดพลาดควรเพียงพอในการดีบักสร้างเท่านั้น
csharptest.net

3

การเพิ่มวิธีโอเวอร์โหลดเพื่อลดการใช้งานพารามิเตอร์เริ่มต้น

ชนิดของการหยุดพัก: การเปลี่ยนแปลงความหมายที่เงียบสงบระดับแหล่งที่มา

เนื่องจากคอมไพเลอร์แปลงการเรียกเมธอดโดยไม่มีค่าพารามิเตอร์ดีฟอลต์เป็นการเรียกอย่างชัดแจ้งพร้อมกับค่าดีฟอลต์บนฝั่งการโทรจึงให้ความเข้ากันได้สำหรับโค้ดคอมไพล์ที่มีอยู่ วิธีการที่มีลายเซ็นที่ถูกต้องจะพบรหัสที่รวบรวมไว้ก่อนหน้านี้ทั้งหมด

ในอีกด้านหนึ่งการโทรที่ไม่มีการใช้พารามิเตอร์ที่เป็นตัวเลือกจะถูกรวบรวมเป็นการเรียกไปยังเมธอดใหม่ที่ไม่มีพารามิเตอร์ทางเลือก ทุกอย่างยังคงทำงานได้ดี แต่ถ้ารหัสที่เรียกนั้นอยู่ในชุดประกอบอื่นการรวบรวมรหัสใหม่ที่เรียกมันจะขึ้นอยู่กับเวอร์ชั่นใหม่ของชุดประกอบนี้ การปรับใช้แอสเซมบลีเรียกรหัส refactored โดยไม่มีการปรับใช้แอสเซมบลี refactored รหัสอยู่ในนั้นเป็นผลในข้อยกเว้น "วิธีไม่พบ"

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

1

เปลี่ยนชื่ออินเทอร์เฟซ

Kinda of Break: Source และBinary

ภาษาที่ได้รับผลกระทบ: น่าจะเป็นทั้งหมดที่ทดสอบใน 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

1

วิธีการบรรทุกเกินพิกัดพร้อมพารามิเตอร์ประเภท nullable

ชนิด: ตัวแบ่งระดับซอร์ส

ภาษาที่ได้รับผลกระทบ: 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);

ข้อยกเว้น: การโทรไม่ชัดเจนระหว่างวิธีการหรือคุณสมบัติต่อไปนี้


0

โปรโมชั่นเป็นวิธีการขยาย

ชนิด: ตัวแบ่งระดับซอร์ส

ภาษาที่ได้รับผลกระทบ: 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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.