เหตุใดการเรียกเมธอดในคลาสที่ได้รับของฉันจึงเรียกเมธอดคลาสพื้นฐาน


146

พิจารณารหัสนี้:

class Program
{
    static void Main(string[] args)
    {
        Person person = new Teacher();
        person.ShowInfo();
        Console.ReadLine();
    }
}

public class Person
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}
public class Teacher : Person
{
    public new void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

เมื่อฉันเรียกใช้รหัสนี้ผลลัพธ์ต่อไปนี้:

ฉันเป็นคน

แต่คุณจะเห็นว่ามันเป็นตัวอย่างของการไม่ได้ของTeacher Personทำไมรหัสถึงทำเช่นนั้น?


3
คำถามจากบุคคล Java: คือ Console.ReadLine (); จำเป็นสำหรับตัวอย่างนี้หรือไม่
รวย

2
@Shahrooz ฉันไม่สามารถตอบคำถามของคุณได้ - ฉันไม่รู้ C # ฉันถามคำถาม C # เล็กน้อยซึ่งนั่นคือการเรียก ReadLine ในวิธีการหลักนั้นจำเป็นเพื่อให้สามารถเรียกใช้ WriteLine ในชั้นเรียนบุคคลและครูได้หรือไม่
รวย

6
ใช่. Net จะปิดหน้าต่างคอนโซลโดยอัตโนมัติเมื่อออกจาก Main () ในการหลีกเลี่ยงสิ่งนี้เราใช้ Console.Read () หรือ Console.Readline () เพื่อรออินพุตเพิ่มเติมเพื่อให้คอนโซลเปิดอยู่
กัปตัน Kenpachi

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

1
@AakashM ขอบคุณ - ฉันใช้เวลาของฉันใน Eclipse ที่คอนโซลเป็นส่วนหนึ่งของหน้าต่าง Eclipse และไม่ได้ปิด นั่นทำให้รู้สึกที่สมบูรณ์แบบ
รวย

คำตอบ:


368

มีความแตกต่างระหว่างการเป็นnewและ/virtualoverride

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

ภาพประกอบของการใช้งานวิธีการ

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

1. คลาสนามธรรม

abstractคนแรกคือ abstractวิธีการเพียงชี้ไปที่:

ภาพประกอบของคลาสนามธรรม

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

public abstract class Person
{
    public abstract void ShowInfo();
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a teacher!");
    }
}

public class Student : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a student!");
    }
}

หากเรียกว่าพฤติกรรมของShowInfoแตกต่างกันไปขึ้นอยู่กับการใช้งาน:

Person person = new Teacher();
person.ShowInfo();    // Shows 'I am a teacher!'

person = new Student();
person.ShowInfo();    // Shows 'I am a student!'

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

ดังนั้นสิ่งที่เกิดขึ้นอยู่เบื้องหลังเมื่อคุณได้รับมรดกจากPerson? เมื่อใช้ShowInfoตัวชี้จะไม่ได้ชี้ไปที่ไหนใด ๆ อีกต่อไปได้ในขณะนี้ชี้ให้เห็นถึงการดำเนินงานที่เกิดขึ้นจริง! เมื่อสร้างStudentอินสแตนซ์มันจะชี้ไปStudentที่ShowInfo:

ภาพประกอบของวิธีการที่สืบทอด

2. วิธีการเสมือนจริง

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

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am a person!");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a teacher!");
    }
}

แตกต่างที่สำคัญคือว่าสมาชิกฐานPerson.ShowInfoไม่ได้ชี้ไปที่ไหนใด ๆ อีกต่อไป นี่คือเหตุผลว่าทำไมคุณสามารถสร้างอินสแตนซ์ของPerson(และทำให้ไม่จำเป็นต้องทำเครื่องหมายเป็นabstractอีกต่อไป):

ภาพประกอบของสมาชิกเสมือนภายในคลาสพื้นฐาน

คุณควรสังเกตว่านี่ไม่ได้ดูแตกต่างจากภาพแรกในตอนนี้ นี่เป็นเพราะvirtualวิธีการชี้ไปที่การใช้งาน " วิธีมาตรฐาน " ใช้virtualคุณสามารถบอกได้Personsว่าพวกเขาสามารถ (ไม่ต้อง ) ShowInfoจัดให้มีการดำเนินงานที่แตกต่างกันสำหรับ ถ้าคุณให้การดำเนินงานที่แตกต่างกัน (ใช้override) เช่นผมสำหรับข้างต้นภาพจะมีลักษณะเช่นเดียวกับTeacher abstractลองนึกภาพเราไม่ได้ให้การปรับใช้ที่กำหนดเองสำหรับStudents:

public class Student : Person
{
}

รหัสจะถูกเรียกเช่นนี้:

Person person = new Teacher();
person.ShowInfo();    // Shows 'I am a teacher!'

person = new Student();
person.ShowInfo();    // Shows 'I am a person!'

และภาพStudentจะมีลักษณะเช่นนี้:

ภาพประกอบการเริ่มต้นใช้งานวิธีการโดยใช้คำหลักเสมือน

3. คำสำคัญ 'ใหม่' อาคาชื่อ "เงา"

newมีแฮ็คมากกว่านี้ คุณสามารถให้วิธีการในชั้นเรียนทั่วไปที่มีชื่อเดียวกันกับวิธีการในชั้นเรียน / อินเตอร์เฟซฐาน ทั้งสองชี้ไปที่การใช้งานที่กำหนดเอง:

ภาพประกอบวิธี "รอบ" โดยใช้คำหลักใหม่

การใช้งานมีลักษณะเหมือนที่คุณให้ไว้ พฤติกรรมนั้นแตกต่างกันไปตามวิธีการเข้าถึงวิธีการของคุณ:

Teacher teacher = new Teacher();
Person person = (Person)teacher;

teacher.ShowInfo();    // Prints 'I am a teacher!'
person.ShowInfo();     // Prints 'I am a person!'

พฤติกรรมนี้สามารถต้องการได้ แต่ในกรณีของคุณมันทำให้เข้าใจผิด

ฉันหวังว่านี่จะทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้นสำหรับคุณ!


9
ขอบคุณสำหรับคำตอบที่ดีของคุณ

6
คุณใช้อะไรในการสร้างไดอะแกรมเหล่านั้น
BlueRaja - Danny Pflughoeft

2
คำตอบที่ยอดเยี่ยมและละเอียดมาก
Nik Bougalis

8
tl; drคุณใช้newซึ่งแบ่งการสืบทอดของฟังก์ชันและทำให้ฟังก์ชันใหม่แยกจากฟังก์ชัน superclass '
ratchet freak

3
@Taymon: จริง ๆ แล้วไม่ ... ฉันแค่ต้องการที่จะทำให้ชัดเจนว่าตอนนี้โทรไปPersonไม่ได้Student;)
Carsten

45

polymorphism ชนิดย่อยใน C # ใช้ virtuality ที่ชัดเจนคล้ายกับ C ++ แต่ต่างจาก Java ซึ่งหมายความว่าคุณต้องทำเครื่องหมายวิธีเป็น overridable (เช่นvirtual) อย่างชัดเจน ใน C # คุณต้องทำเครื่องหมายวิธีการเอาชนะอย่างชัดเจนว่าเป็นการแทนที่ (เช่นoverride) เพื่อป้องกันการพิมพ์ผิด

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

ในรหัสในคำถามของคุณคุณใช้newซึ่งทำเงาแทนการเอาชนะ การแรเงามีผลกับซีแมนทิกส์เวลารวบรวมเท่านั้นไม่ใช่ซีแมนทิกต์รันไทม์


4
ใครจะบอกว่า OP รู้ว่าพวกเขาหมายถึงอะไร
โคลจอห์นสัน

@ColeJohnson ฉันจะเพิ่มคำอธิบาย

25

คุณต้องสร้างเมธอดเสมือนและคุณต้องแทนที่ฟังก์ชันในคลาสลูกเพื่อเรียกเมธอดของคลาสอ็อบเจ็กต์ที่คุณใส่ในการอ้างอิงคลาสพาเรนต์

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}
public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

วิธีการเสมือน

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

ใช้ใหม่สำหรับ Shadowing

คุณกำลังใช้คำสำคัญใหม่แทนการแทนที่นี่คือสิ่งที่ทำใหม่

  • หากวิธีการในชั้นเรียนที่ได้รับไม่ได้นำหน้าด้วยคำหลักใหม่หรือแทนที่, คอมไพเลอร์จะออกคำเตือนและวิธีการที่จะทำงานราวกับว่าคำหลักใหม่ที่มีอยู่

  • หากวิธีการในชั้นเรียนมาจะนำหน้าด้วยคำหลักใหม่วิธีการที่ถูกกำหนดให้เป็นอิสระของวิธีการในชั้นฐานที่นี้บทความ MSDNอธิบายได้เป็นอย่างดี

การรวมก่อนหน้า

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

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

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


7

ฉันต้องการที่จะสร้างออกจากคำตอบของ Achratt เพื่อความสมบูรณ์ความแตกต่างคือ OP คาดหวังว่าnewคำสำคัญในวิธีการเรียนที่ได้รับเพื่อแทนที่วิธีการเรียนระดับพื้นฐาน สิ่งที่มันทำจริงๆคือซ่อนเมธอดคลาสพื้นฐาน

ใน C # ตามคำตอบอื่นที่กล่าวถึงวิธีการดั้งเดิมที่จะต้องชัดเจน เมธอดคลาสฐานต้องถูกทำเครื่องหมายเป็นvirtualและคลาสที่ได้รับต้องoverrideเมธอดคลาสพื้นฐานโดยเฉพาะ ถ้าสิ่งนี้ทำเสร็จแล้วมันไม่สำคัญว่าวัตถุนั้นจะถือว่าเป็นตัวอย่างของคลาสพื้นฐานหรือคลาสที่ได้รับ พบวิธีการที่ได้รับและเรียกว่า สิ่งนี้ทำในลักษณะเดียวกันกับใน C ++; วิธีการที่ระบุว่า "virtual" หรือ "override" เมื่อรวบรวมจะได้รับการแก้ไข "ล่าช้า" (ที่รันไทม์) โดยการกำหนดประเภทที่แท้จริงของวัตถุที่อ้างอิงและการข้ามลำดับชั้นของวัตถุลงไปตามต้นไม้จากประเภทตัวแปรเป็นประเภทวัตถุจริง เพื่อค้นหาการใช้งานที่ได้รับมากที่สุดของวิธีการที่กำหนดโดยประเภทตัวแปร

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

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

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

นอกจากนี้newคำหลักไม่เพียง แต่จะทำสิ่งนี้อย่างชัดเจนเท่านั้น C # ยังอนุญาตให้ซ่อนเมธอดโดยนัยได้ เพียงกำหนดวิธีการที่มีลายเซ็นเดียวกับวิธีการเรียนระดับผู้ปกครองโดยไม่ต้องoverrideหรือnewจะซ่อนมัน (แม้ว่ามันจะผลิตคำเตือนคอมไพเลอร์หรือการร้องเรียนจากผู้ช่วย refactoring เช่น ReSharper หรือ CodeRush) นี่คือการประนีประนอมของนักออกแบบ C # ที่เกิดขึ้นระหว่างการแทนที่โดยชัดแจ้งของ C ++ กับ Java โดยปริยายและแม้ว่ามันจะสง่างาม แต่มันก็ไม่ได้สร้างพฤติกรรมที่คุณคาดหวังหากคุณมาจากพื้นหลังในภาษาเก่า

นี่คือสิ่งใหม่: สิ่งนี้มีความซับซ้อนเมื่อคุณรวมคำหลักสองคำในห่วงโซ่การสืบทอดที่ยาวนาน พิจารณาสิ่งต่อไปนี้:

class Foo { public virtual void DoFoo() { Console.WriteLine("Foo"); } }
class Bar:Foo { public override sealed void DoFoo() { Console.WriteLine("Bar"); } }
class Baz:Bar { public virtual void DoFoo() { Console.WriteLine("Baz"); } }
class Bai:Baz { public override void DoFoo() { Console.WriteLine("Bai"); } }
class Bat:Bai { public new void DoFoo() { Console.WriteLine("Bat"); } }
class Bak:Bat { }

Foo foo = new Foo();
Bar bar = new Bar();
Baz baz = new Baz();
Bai bai = new Bai();
Bat bat = new Bat();

foo.DoFoo();
bar.DoFoo();
baz.DoFoo();
bai.DoFoo();
bat.DoFoo();

Console.WriteLine("---");

Foo foo2 = bar;
Bar bar2 = baz;
Baz baz2 = bai;
Bai bai2 = bat;
Bat bat2 = new Bak();

foo2.DoFoo();
bar2.DoFoo();
baz2.DoFoo();
bai2.DoFoo();    

Console.WriteLine("---");

Foo foo3 = bak;
Bar bar3 = bak;
Baz baz3 = bak;
Bai bai3 = bak;
Bat bat3 = bak;

foo3.DoFoo();
bar3.DoFoo();
baz3.DoFoo();
bai3.DoFoo();    
bat3.DoFoo();

เอาท์พุท:

Foo
Bar
Baz
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat

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

ชุดที่สองห้าคือผลลัพธ์ของการกำหนดแต่ละอินสแตนซ์ให้กับตัวแปรของชนิดพาเรนต์ทันที ตอนนี้พฤติกรรมที่แตกต่างกันก็สะบัดออกไป foo2ซึ่งจริงๆแล้วBarนักแสดงเป็น a Fooจะยังคงพบวิธีที่ได้รับเพิ่มเติมของแถบวัตถุชนิดจริง bar2เป็นBazแต่ไม่เหมือนfoo2กันเนื่องจาก Baz ไม่ได้แทนที่การใช้งานของ Bar อย่างชัดเจน (ไม่สามารถทำได้; ไม่สามารถทำได้ Bar sealed) จึงไม่เห็นรันไทม์เมื่อค้นหา "จากบนลงล่าง" ดังนั้นการใช้งานของ Bar จึงถูกเรียกใช้แทน โปรดสังเกตว่า Baz ไม่จำเป็นต้องใช้newคำหลัก คุณจะได้รับคำเตือนของคอมไพเลอร์หากคุณไม่ใช้คำหลัก แต่พฤติกรรมโดยนัยใน C # คือการซ่อนวิธีการหลัก baz2คือ a Bai, ซึ่งแทนที่Bazของnewการนำไปปฏิบัติดังนั้นพฤติกรรมของมันจึงคล้ายกับfoo2ของ การใช้งานประเภทวัตถุจริงใน Bai เรียกว่า bai2คือ a Batซึ่งซ่อนBaiการใช้งานเมธอดของผู้ปกครองอีกครั้งและมันก็ทำงานเหมือนกันกับbar2การใช้งานของ Bai ที่ไม่ได้ถูกปิดผนึกดังนั้น Bat ในทางทฤษฎีจะสามารถแทนที่ได้แทนที่จะซ่อนวิธีการ ในที่สุด, bat2คือBak, ซึ่งไม่มีการเอาชนะการดำเนินการของทั้งสองชนิด, และเพียงแค่ใช้มันจากแม่ของมัน.

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


4

โปรดอ่านเกี่ยวกับความหลากหลายใน C #: ความหลากหลาย (คู่มือการเขียนโปรแกรม C #)

นี่คือตัวอย่างจากที่นั่น:

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

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

3

คุณต้องทำมันvirtualแล้วแทนที่ฟังก์ชั่นTeacherนั้น virtualในขณะที่คุณกำลังสืบทอดและใช้ตัวชี้ฐานในการอ้างถึงชั้นมาคุณจะต้องแทนที่โดยใช้ newมีไว้สำหรับซ่อนbaseเมธอดคลาสบนการอ้างอิงคลาสที่ได้รับไม่ใช่การbaseอ้างอิงคลาส


3

ฉันต้องการเพิ่มตัวอย่างอีกสองสามตัวอย่างเพื่อขยายข้อมูลเกี่ยวกับสิ่งนี้ หวังว่านี่จะช่วยได้เช่นกัน:

นี่คือตัวอย่างโค้ดที่จะกำจัดอากาศรอบ ๆ สิ่งที่เกิดขึ้นเมื่อกำหนดประเภทที่ได้รับมาให้กับประเภทฐาน มีวิธีใดบ้างที่ใช้ได้และความแตกต่างระหว่างวิธีการแทนที่และวิธีซ่อนเร้นในบริบทนี้

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.foo();        // A.foo()
            a.foo2();       // A.foo2()

            a = new B();    
            a.foo();        // B.foo()
            a.foo2();       // A.foo2()
            //a.novel() is not available here

            a = new C();
            a.foo();        // C.foo()
            a.foo2();       // A.foo2()

            B b1 = (B)a;    
            b1.foo();       // C.foo()
            b1.foo2();      // B.foo2()
            b1.novel();     // B.novel()

            Console.ReadLine();
        }
    }


    class A
    {
        public virtual void foo()
        {
            Console.WriteLine("A.foo()");
        }

        public void foo2()
        {
            Console.WriteLine("A.foo2()");
        }
    }

    class B : A
    {
        public override void foo()
        {
            // This is an override
            Console.WriteLine("B.foo()");
        }

        public new void foo2()      // Using the 'new' keyword doesn't make a difference
        {
            Console.WriteLine("B.foo2()");
        }

        public void novel()
        {
            Console.WriteLine("B.novel()");
        }
    }

    class C : B
    {
        public override void foo()
        {
            Console.WriteLine("C.foo()");
        }

        public new void foo2()
        {
            Console.WriteLine("C.foo2()");
        }
    }
}

ความผิดปกติเล็ก ๆ น้อย ๆ อีกอย่างหนึ่งก็คือสำหรับรหัสบรรทัดต่อไปนี้:

A a = new B();    
a.foo(); 

คอมไพเลอร์ VS (Intellisense) จะแสดง a.foo () เป็น A.foo ()

ดังนั้นจึงเป็นที่ชัดเจนว่าเมื่อประเภทที่ได้รับมากกว่าถูกกำหนดให้กับประเภทฐานตัวแปร 'ประเภทฐาน' ทำหน้าที่เป็นประเภทฐานจนกว่าวิธีการที่ถูกแทนที่ในประเภทที่ได้รับการอ้างอิง สิ่งนี้อาจกลายเป็นเรื่องโต้กลับเล็กน้อยด้วยวิธีการที่ซ่อนอยู่หรือวิธีการที่มีชื่อเดียวกัน (แต่ไม่แทนที่) ระหว่างประเภทแม่และลูก

ตัวอย่างโค้ดนี้ควรช่วยวิเคราะห์คำเตือนเหล่านี้!


2

C # แตกต่างจาก java ในพฤติกรรมการแทนที่คลาส parent / child ตามค่าเริ่มต้นใน Java วิธีการทั้งหมดเป็นเสมือนดังนั้นพฤติกรรมที่คุณต้องการได้รับการสนับสนุนนอกกรอบ

ใน C # คุณต้องทำเครื่องหมายวิธีเสมือนในคลาสพื้นฐานแล้วคุณจะได้รับสิ่งที่คุณต้องการ


2

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


1

ประเภทของตัวแปร 'อาจารย์' ที่นี่คือtypeof(Person)และประเภทนี้ไม่ทราบอะไรเกี่ยวกับชั้นเรียนของครูและไม่พยายามที่จะมองหาวิธีการใด ๆ ในประเภทที่ได้รับ ในการเรียกวิธีการเรียนของคุณครูคุณควรแปลงตัวแปรของคุณ: (person as Teacher).ShowInfo().

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

public class Program
{
    private static void Main(string[] args)
    {
        Person teacher = new Teacher();
        teacher.ShowInfo();

        Person incognito = new IncognitoPerson ();
        incognito.ShowInfo();

        Console.ReadLine();
    }
}

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

public class IncognitoPerson : Person
{

}

1

อาจจะสายเกินไป ... แต่คำถามนั้นง่ายและคำตอบควรมีระดับความซับซ้อนเท่ากัน

ในตัวแปรรหัสของคุณไม่รู้เกี่ยวกับ Teacher.ShowInfo () ไม่มีวิธีการเรียกวิธีสุดท้ายจากการอ้างอิงคลาสพื้นฐานเนื่องจากไม่ใช่เสมือน

มีวิธีที่มีประโยชน์ในการสืบทอด - ลองจินตนาการว่าคุณต้องการพูดอะไรกับลำดับชั้นรหัสของคุณ ลองจินตนาการดูว่าเครื่องมือตัวใดตัวหนึ่งหรืออย่างอื่นพูดเกี่ยวกับตัวเอง เช่นถ้าคุณเพิ่มฟังก์ชั่นเสมือนลงในคลาสพื้นฐานที่คุณคิดว่า: 1 มันสามารถมีการใช้งานเริ่มต้น; 2. มันอาจจะนำมาใช้ใหม่ในชั้นเรียนที่ได้รับ ถ้าคุณเพิ่มฟังก์ชั่นนามธรรมมันหมายถึงสิ่งเดียวเท่านั้น - คลาสย่อยจะต้องสร้างการใช้งาน แต่ในกรณีที่คุณมีฟังก์ชั่นธรรมดา - คุณไม่คาดหวังว่าใครจะเปลี่ยนการใช้งาน


0

Teacherคอมไพเลอร์นี้ไม่ได้เพราะมันไม่ได้รู้ว่ามันเป็น ทั้งหมดที่รู้ก็คือมันเป็นPersonสิ่งที่ได้มาจากมัน ดังนั้นสิ่งที่สามารถทำได้คือเรียกPerson.ShowInfo()วิธีการ


0

แค่อยากจะให้คำตอบสั้น ๆ -

คุณควรใช้virtualและoverrideในชั้นเรียนที่อาจถูกแทนที่ ใช้virtualสำหรับวิธีการที่สามารถ overriden โดยคลาสลูกและใช้overrideสำหรับวิธีการที่ควรแทนที่virtualวิธีดังกล่าว


0

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

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

public class inheritance{

    public static void main(String[] args){

        Person person = new Teacher();
        person.ShowInfo();
    }
}

class Person{

    public void ShowInfo(){
        System.out.println("I am Person");
    }
}

class Teacher extends Person{

    public void ShowInfo(){
        System.out.println("I am Teacher");
    }
}

0

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

เนมสเปซ LinqConsoleApp {

class Program
{

    static void Main(string[] args)
    {


        Person person = new Teacher();
        Console.Write(GetMemberName(() => person) + ": ");
        person.ShowInfo();

        Teacher teacher = new Teacher();
        Console.Write(GetMemberName(() => teacher) + ": ");
        teacher.ShowInfo();

        IPerson person1 = new Teacher();
        Console.Write(GetMemberName(() => person1) + ": ");
        person1.ShowInfo();

        IPerson person2 = (IPerson)teacher;
        Console.Write(GetMemberName(() => person2) + ": ");
        person2.ShowInfo();

        Teacher teacher1 = (Teacher)person1;
        Console.Write(GetMemberName(() => teacher1) + ": ");
        teacher1.ShowInfo();

        Person person4 = new Person();
        Console.Write(GetMemberName(() => person4) + ": ");
        person4.ShowInfo();

        IPerson person3 = new Person();
        Console.Write(GetMemberName(() => person3) + ": ");
        person3.ShowInfo();

        Console.WriteLine();

        Console.ReadLine();

    }

    private static string GetMemberName<T>(Expression<Func<T>> memberExpression)
    {
        MemberExpression expressionBody = (MemberExpression)memberExpression.Body;
        return expressionBody.Member.Name;
    }

}
interface IPerson
{
    void ShowInfo();
}
public class Person : IPerson
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Person == " + this.GetType());
    }
    void IPerson.ShowInfo()
    {
        Console.WriteLine("I am interface Person == " + this.GetType());
    }
}
public class Teacher : Person, IPerson
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Teacher == " + this.GetType());
    }
}

}

นี่คือผลลัพธ์:

person: I am Person == LinqConsoleApp.Teacher

ครู: ฉันเป็นครู == LinqConsoleApp.Teacher

person1: ฉันเป็นครู == LinqConsoleApp.Teacher

person2: ฉันเป็นครู == LinqConsoleApp.Teacher

teacher1: ฉันเป็นครู == LinqConsoleApp.Teacher

person4: ฉันเป็นคน == LinqConsoleApp.Person

person3: ฉันเป็นอินเทอร์เฟซ Person == LinqConsoleApp.Person

สองสิ่งที่ควรทราบ:
วิธีการ Teacher.ShowInfo () ตัดคำหลักใหม่ออก เมื่อละเว้นใหม่ลักษณะการทำงานของเมธอดจะเหมือนกับว่ามีการกำหนดคำหลักใหม่อย่างชัดเจน

คุณสามารถใช้คีย์เวิร์ด override ร่วมกับคีย์เวิร์ด virtual เท่านั้น เมธอดคลาสพื้นฐานต้องเป็นเสมือน หรือนามธรรมซึ่งในกรณีนี้คลาสจะต้องเป็นนามธรรม

person ได้รับการติดตั้งพื้นฐานของ ShowInfo เนื่องจากคลาส Teacher ไม่สามารถแทนที่การใช้งานพื้นฐาน (ไม่มีการประกาศเสมือน) และ person คือ .GetType (Teacher) ดังนั้นจึงซ่อนการใช้งานของคลาส Teacher ไว้

ครูได้รับการใช้งานครูที่ได้รับจาก ShowInfo เพราะครูเพราะเป็น Typeof (อาจารย์) และไม่ได้อยู่ในระดับการสืบทอดบุคคล

person1 ได้รับการใช้งานครูที่ได้รับเนื่องจากเป็น. GetType (Teacher) และคำหลักใหม่โดยนัยจะซ่อนการใช้งานพื้นฐาน

person2 ยังได้รับการนำไปปฏิบัติของครูแม้ว่าจะใช้ IPerson และได้รับการส่งต่อไปยัง IPerson อย่างชัดเจน นี่เป็นอีกครั้งเนื่องจากคลาสของครูไม่ได้ใช้วิธี IPerson.ShowInfo () อย่างชัดเจน

teacher1 ยังได้รับการใช้งานครูที่ได้รับเนื่องจากเป็น. GetType (อาจารย์)

person3 เท่านั้นที่ได้รับการนำ IPerson ไปใช้ของ ShowInfo เนื่องจากเฉพาะคลาส Person เท่านั้นที่ใช้เมธอดและ person3 เป็นตัวอย่างของชนิด IPerson

ในการใช้อินเตอร์เฟสอย่างชัดเจนคุณต้องประกาศอินสแตนซ์ var ของชนิดอินเตอร์เฟสเป้าหมายและคลาสต้องใช้ (มีคุณสมบัติครบถ้วน) สมาชิกอินเทอร์เฟซอย่างชัดเจน

แจ้งให้ทราบไม่แม้แต่ person4 รับ IPerson.ShowInfo การใช้งาน นี่เป็นเพราะแม้ว่า person4 เป็น .GetType (Person) และแม้ว่า Person จะใช้ IPerson แต่ person4 ไม่ใช่อินสแตนซ์ของ IPerson


ฉันเห็นว่าการจัดรูปแบบรหัสอย่างถูกต้องนำเสนอความท้าทายเล็กน้อย ไม่มีเวลาที่จะสวยขึ้นตอนนี้ ...
แข็ง

0

ตัวอย่าง LinQPad ที่จะเปิดใช้งานแบบสุ่มสี่สุ่มห้าและลดความซ้ำซ้อนของรหัสซึ่งฉันคิดว่าเป็นสิ่งที่คุณพยายามทำ

void Main()
{
    IEngineAction Test1 = new Test1Action();
    IEngineAction Test2 = new Test2Action();
    Test1.Execute("Test1");
    Test2.Execute("Test2");
}

public interface IEngineAction
{
    void Execute(string Parameter);
}

public abstract class EngineAction : IEngineAction
{
    protected abstract void PerformAction();
    protected string ForChildren;
    public void Execute(string Parameter)
    {  // Pretend this method encapsulates a 
       // lot of code you don't want to duplicate 
      ForChildren = Parameter;
      PerformAction();
    }
}

public class Test1Action : EngineAction
{
    protected override void PerformAction()
    {
        ("Performed: " + ForChildren).Dump();
    }
}

public class Test2Action : EngineAction
{
    protected override void PerformAction()
    {
        ("Actioned: " + ForChildren).Dump();
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.