เหตุใดฉันจึงไม่มีวิธีคงที่เชิงนามธรรมใน C #


182

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


3
โปรดเปิดไว้เพื่อเปิดโอกาสให้มีการปรับปรุงในอนาคต
Mark Biek

6
ฉันคิดว่าคำถามกำลังได้รับจากข้อเท็จจริงที่ว่า C # ต้องการคำหลักอื่นสำหรับสถานการณ์เช่นนี้อย่างแม่นยำ คุณต้องการวิธีที่ค่าส่งคืนขึ้นอยู่กับชนิดที่มันถูกเรียก คุณไม่สามารถเรียกมันว่า "คงที่" หากไม่รู้จักประเภทดังกล่าว แต่เมื่อทราบชนิดแล้วจะกลายเป็นแบบคงที่ "Unresolved Static" เป็นแนวคิด - มันยังไม่คงที่ แต่เมื่อเราทราบชนิดที่ได้รับแล้วจะเป็น นี่เป็นแนวคิดที่ดีอย่างสมบูรณ์แบบซึ่งเป็นเหตุผลที่โปรแกรมเมอร์ขอต่อไป แต่มันก็ไม่สอดคล้องกับวิธีที่นักออกแบบคิดเกี่ยวกับภาษา
William Jockusch

@WilliamJockusch ประเภทการรับหมายถึงอะไร ถ้าฉันเรียก BaseClass.StaticMethod () ดังนั้น BaseClass เป็นประเภทเดียวที่สามารถใช้ในการตัดสินใจ แต่ในระดับนี้มันเป็นนามธรรมดังนั้นวิธีการไม่สามารถแก้ไขได้ ถ้าคุณเรียก DerivedClass.StaticMethod แทนดีคลาสฐานไม่เกี่ยวข้อง
Martin Capodici

ในคลาสพื้นฐานเมธอดไม่ได้รับการแก้ไขและคุณไม่สามารถใช้ได้ คุณต้องการประเภทที่ได้รับหรือวัตถุ (ซึ่งจะมีประเภทที่ได้รับมา) คุณควรจะสามารถเรียก baseClassObject.Method () หรือ DerivedClass.Method () คุณไม่สามารถเรียก BaseClass.Method () ได้เนื่องจากไม่ได้ให้ประเภทแก่คุณ
William Jockusch

คำตอบ:


157

วิธีการคงที่ไม่ได้ยกตัวอย่างเช่นนี้พวกเขาก็ใช้ได้โดยไม่มีการอ้างอิงวัตถุ

การเรียกใช้เมธอดแบบสแตติกทำผ่านชื่อคลาสไม่ใช่ผ่านการอ้างอิงวัตถุและรหัส Intermediate Language (IL) เพื่อเรียกใช้จะเรียกเมธอด abstract ผ่านชื่อของคลาสที่กำหนดไว้ไม่จำเป็นต้องเป็นชื่อของ ชั้นเรียนที่คุณใช้

ให้ฉันแสดงตัวอย่าง

ด้วยรหัสต่อไปนี้:

public class A
{
    public static void Test()
    {
    }
}

public class B : A
{
}

ถ้าคุณโทรหา B.Test แบบนี้:

class Program
{
    static void Main(string[] args)
    {
        B.Test();
    }
}

จากนั้นโค้ดจริงภายในเมธอด Main จะเป็นดังนี้:

.entrypoint
.maxstack 8
L0000: nop 
L0001: call void ConsoleApplication1.A::Test()
L0006: nop 
L0007: ret 

อย่างที่คุณเห็นการโทรนั้นทำกับ A.Test เพราะเป็นคลาส A ที่กำหนดไว้และไม่ใช่การทดสอบ B.Test แม้ว่าคุณจะสามารถเขียนรหัสได้

หากคุณมีประเภทคลาสเช่นใน Delphi ซึ่งคุณสามารถสร้างตัวแปรที่อ้างอิงถึงชนิดและไม่ใช่วัตถุคุณจะต้องใช้วิธีเสมือนและนามธรรมคงที่ (และคอนสตรัคเตอร์) แต่ก็ไม่มีให้ใช้และ ดังนั้นการโทรคงไม่ใช่เสมือนใน. NET

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

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

ดังนั้นวิธีการแบบคงที่เสมือน / นามธรรมจะไม่พร้อมใช้งานใน. NET


4
เมื่อรวมกับวิธีดำเนินการโอเวอร์โหลดตัวดำเนินการใน C # สิ่งนี้น่าจะช่วยลดความเป็นไปได้ในการกำหนดคลาสย่อยเพื่อให้มีการใช้งานสำหรับตัวดำเนินการโอเวอร์โหลดที่กำหนด
Chris Moschini

23
ฉันไม่พบคำตอบนี้มีประโยชน์มากเพราะคำจำกัดความของTest()มันมีความหมายAมากกว่าจะเป็นนามธรรมและอาจนิยามBได้ \

5
พารามิเตอร์ชนิดทั่วไปทำงานอย่างมีประสิทธิภาพเป็นตัวแปร "ชนิด" ไม่คงอยู่และวิธีการแบบคงที่เสมือนอาจมีประโยชน์ในบริบทดังกล่าว ตัวอย่างเช่นถ้ามีCarประเภทที่มีการคงเสมือนCreateFromDescriptionการโรงงานแล้วรหัสที่ได้รับการยอมรับCarประเภททั่วไป -constrained TสามารถโทรในการผลิตรถประเภทT.CreateFromDescription Tโครงสร้างดังกล่าวสามารถรองรับได้ค่อนข้างดีภายใน CLR หากแต่ละประเภทที่กำหนดวิธีการดังกล่าวถือเป็นอินสแตนซ์ singleton แบบคงที่ของคลาสที่ซ้อนกันทั่วไปซึ่งมีวิธีการ "คงที่" เสมือนจริง
supercat

45

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

สมมติว่าคุณทำได้โดยสืบทอดวิธีคงที่สักครู่ ลองนึกภาพสถานการณ์นี้:

public static class Base
{
    public static virtual int GetNumber() { return 5; }
}

public static class Child1 : Base
{
    public static override int GetNumber() { return 1; }
}

public static class Child2 : Base
{
    public static override int GetNumber() { return 2; }
}

ถ้าคุณเรียก Base.GetNumber () วิธีการใดที่จะเรียก? คืนค่าใด มันค่อนข้างง่ายที่จะเห็นว่าโดยไม่ต้องสร้างอินสแตนซ์ของวัตถุการสืบทอดค่อนข้างยาก วิธีที่เป็นนามธรรมโดยไม่มีการสืบทอดเป็นเพียงวิธีการที่ไม่มีร่างกายดังนั้นจึงไม่สามารถเรียกได้


33
รับสถานการณ์ของคุณฉันจะบอกว่า Base.GetNumber () จะกลับมา 5; Child1.GetNumber () ส่งคืน 1; Child2.GetNumber () ส่งคืน 2; คุณสามารถพิสูจน์ฉันผิดเพื่อช่วยให้เข้าใจเหตุผลของคุณหรือไม่ ขอบคุณ
Luis Filipe

ใบหน้าที่คุณคิดว่า Base.GetNumber () ส่งคืน 5 หมายความว่าคุณเข้าใจแล้วว่าเกิดอะไรขึ้น ด้วยการส่งคืนค่าฐานจะไม่มีการสืบทอดเกิดขึ้น
David Wengier

60
ทำไม Base.GetNumber () ในโลกถึงกลับมาเป็นอย่างอื่นนอกจาก 5? มันเป็นวิธีการในคลาสฐาน - มีเพียง 1 ตัวเลือก
Artem Russakovskii

4
@ArtemRussakovskii: int DoSomething<T>() where T:Base {return T.GetNumber();}สมมติว่าใครมี มันจะมีประโยชน์ถ้าDoSomething<Base>()สามารถคืนได้ห้าในขณะที่DoSomething<Child2>()จะคืนสอง ความสามารถดังกล่าวไม่เพียง แต่จะเป็นประโยชน์สำหรับตัวอย่างของเล่นเท่านั้น แต่ยังรวมถึงสิ่งอื่น ๆ เช่นclass Car {public static virtual Car Build(PurchaseOrder PO);}ที่ทุกชั้นเรียนCarจะต้องกำหนดวิธีการที่สามารถสร้างตัวอย่างที่ได้รับคำสั่งซื้อ
supercat

4
มี "ปัญหา" เหมือนกันกับการสืบทอดแบบไม่คงที่
อาร์คุง

18

ผู้ตอบอีกคนหนึ่ง (McDowell) กล่าวว่า polymorphism นั้นใช้ได้กับอินสแตนซ์ของวัตถุเท่านั้น นั่นควรจะมีคุณสมบัติ; มีภาษาที่ใช้งานคลาสเป็นอินสแตนซ์ของประเภท "Class" หรือ "Metaclass" ภาษาเหล่านี้รองรับความหลากหลายสำหรับทั้งอินสแตนซ์และเมธอดคลาส (สแตติก)

C # เช่น Java และ C ++ ก่อนหน้านั้นไม่ใช่ภาษาดังกล่าว staticคำหลักที่ถูกนำมาใช้อย่างชัดเจนเพื่อแสดงว่าวิธีการที่เป็นแบบคงที่ถูกผูกไว้มากกว่าแบบไดนามิก / เสมือน


9

นี่คือสถานการณ์ที่จำเป็นต้องมีการสืบทอดสำหรับฟิลด์และเมธอดแบบสแตติกแน่นอน:

abstract class Animal
{
  protected static string[] legs;

  static Animal() {
    legs=new string[0];
  }

  public static void printLegs()
  {
    foreach (string leg in legs) {
      print(leg);
    }
  }
}


class Human: Animal
{
  static Human() {
    legs=new string[] {"left leg", "right leg"};
  }
}


class Dog: Animal
{
  static Dog() {
    legs=new string[] {"left foreleg", "right foreleg", "left hindleg", "right hindleg"};
  }
}


public static void main() {
  Dog.printLegs();
  Human.printLegs();
}


//what is the output?
//does each subclass get its own copy of the array "legs"?

4
ไม่มี 'ขา' ของแถวลำดับเดียวเท่านั้น เอาท์พุทเป็น nondeterministic เพราะคุณไม่รู้ว่าคำสั่งของตัวสร้างสแตติกจะถูกเรียกว่าอะไร 'Need' เป็นคำที่ค่อนข้างสมบูรณ์โดยที่ 'ปรารถนา' น่าจะแม่นยำกว่า
แซม

legsควรเป็นคุณสมบัตินามธรรมแบบคงที่
Endian น้อย

8

เมื่อต้องการเพิ่มคำอธิบายก่อนหน้านี้การเรียกใช้เมธอดแบบสแตติกจะถูกผูกไว้กับวิธีการเฉพาะในเวลาคอมไพล์ซึ่งค่อนข้างออกกฎพฤติกรรม polymorphic


C # ถูกพิมพ์แบบคงที่; การเรียกใช้วิธี polymorphic นั้นถูกผูกไว้ในเวลารวบรวมตามที่ฉันเข้าใจ - กล่าวคือ CLR ไม่ได้ถูกทิ้งให้แก้ไขวิธีที่จะเรียกใช้ในขณะรันไทม์
Adam Tolley

ดังนั้นคุณคิดว่า polymorphism ทำงานบน CLR ได้อย่างไร? คำอธิบายของคุณเพิ่งตัดทอนวิธีการเสมือนจริง
Rytmis

นั่นไม่ใช่ความคิดเห็นที่มีประโยชน์เท่าที่ควร ฉันเชิญวาทกรรมที่มีประโยชน์ (ด้วย 'ตามที่ฉันเข้าใจ') คิดว่าบางทีคุณอาจให้เนื้อหาเพิ่มเติมอีกหน่อย - เนื่องจากผู้คนมาที่นี่เพื่อค้นหาคำตอบ แม้ว่าดูเหมือนว่าฉันอาจมีความผิดในสิ่งเดียวกัน - ฉันหมายถึงความคิดเห็นข้างต้นเป็นคำถาม: C # ไม่ประเมินสิ่งเหล่านี้ในเวลารวบรวมหรือไม่
Adam Tolley

ขอโทษฉันไม่ได้หมายถึงการดูถูก (แม้ว่าฉันจะยอมรับการตอบสนองอย่างรวดเร็ว ;-) ประเด็นคำถามของฉันคือถ้าคุณมีคลาสเหล่านี้: คลาสฐาน {สาธารณะเสมือนโมฆะวิธี (); } คลาสที่ได้รับ: Base {public override void Method (); } และเขียนดังนี้: Base instance = new Derived (); instance.Method (); ข้อมูลประเภทเวลาคอมไพล์บนไซต์การโทรคือเราได้รับอินสแตนซ์ของ Base เมื่ออินสแตนซ์จริงคือ Derived ดังนั้นคอมไพเลอร์ไม่สามารถแก้ไขวิธีที่แน่นอนในการโทร แต่มันส่งเสียง "callvirt" การเรียนการสอนที่บอก IL รันไทม์การจัดส่ง ..
Rytmis

1
ขอบคุณคนที่ให้ข้อมูล! คิดว่าฉันได้ดำดิ่งสู่ IL นานพอแล้วขอให้ฉันโชคดี
Adam Tolley

5

เราแทนที่เมธอดสแตติก (ในเดลฟาย) จริง ๆ แล้วมันค่อนข้างน่าเกลียด แต่ก็ใช้ได้ดีสำหรับความต้องการของเรา

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

class function AvailableObjects: string; override;
begin
  Result := 'Object1, Object2';
end; 

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

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

ดังนั้นการบำรุงรักษานี้ง่ายกว่าการมีแอปพลิเคชั่นเซิร์ฟเวอร์ที่แตกต่างกันหนึ่งรายการสำหรับไคลเอนต์แต่ละตัว

หวังว่าตัวอย่างจะชัดเจน


0

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


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