ทำไมภาษาการเขียนโปรแกรมอนุญาตให้แชโดว์ / การซ่อนตัวแปรและฟังก์ชั่น?


31

ภาษาการเขียนโปรแกรมที่นิยมมากที่สุด (เช่น C ++, Java, Python ฯลฯ ) มีแนวคิดของการซ่อน / แชโดว์ของตัวแปรหรือฟังก์ชั่น เมื่อฉันพบการซ่อนหรือเงาพวกเขาเป็นสาเหตุของข้อบกพร่องที่หายากและฉันไม่เคยเห็นกรณีที่ฉันพบว่าจำเป็นต้องใช้คุณสมบัติเหล่านี้ของภาษา

สำหรับฉันมันจะเป็นการดีกว่าถ้าหากไม่อนุญาตให้ซ่อนและซ่อนเงา

มีใครรู้บ้างเกี่ยวกับการใช้แนวคิดเหล่านี้

อัปเดต:
ฉันไม่ได้อ้างถึงการห่อหุ้มของสมาชิกในชั้นเรียน (ส่วนตัว / สมาชิกที่ได้รับความคุ้มครอง)


นั่นเป็นเหตุผลที่ชื่อฟิลด์ทั้งหมดของฉันเริ่มต้นด้วย F.
Pieter B

7
ฉันคิดว่า Eric Lippert มีบทความดีๆเกี่ยวกับเรื่องนี้ โอ้เดี๋ยวก่อนนี่คือ: blogs.msdn.com/b/ericlippert/archive/2008/05/21/ …
Lescai Ionel

1
โปรดอธิบายคำถามของคุณ คุณกำลังถามเกี่ยวกับการซ่อนข้อมูลโดยทั่วไปหรือเกี่ยวกับกรณีเฉพาะที่อธิบายไว้ในบทความ Lippert ซึ่งคลาสที่ได้รับจะซ่อนฟังก์ชันของคลาสฐานหรือไม่?
Aaron Kurtzhals

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

คำตอบ:


26

หากคุณไม่อนุญาตให้ซ่อนและแชโดว์สิ่งที่คุณมีคือภาษาที่ตัวแปรทั้งหมดเป็นสากล

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

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

ปัญหาพื้นฐานไม่ได้ซ่อน / แชโดว์ แต่เป็นตัวแปรทั่วโลก


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

19
"หากคุณไม่อนุญาตให้ซ่อนหรือทำเงาสิ่งที่คุณมีคือภาษาที่ตัวแปรทั้งหมดเป็นสากล" - ไม่จำเป็นต้อง: คุณสามารถมีตัวแปรที่กำหนดขอบเขตโดยไม่มีเงาและคุณอธิบายมัน
Thiago Silva

@ThiagoSilva: และแล้วภาษาของคุณจะต้องมีวิธีการที่จะบอกคอมไพเลอร์ที่โมดูลนี้ISได้รับอนุญาตให้เข้าถึงโมดูล "frammis" ตัวแปร คุณอนุญาตให้ใครบางคนซ่อน / เงาวัตถุที่เขาไม่รู้แม้กระทั่งมีอยู่หรือคุณบอกเขาเกี่ยวกับสิ่งนั้นเพื่อบอกเขาว่าทำไมเขาไม่ได้รับอนุญาตให้ใช้ชื่อนั้น
John R. Strohm

9
@ ฟิลขอโทษด้วยที่ไม่เห็นด้วยกับคุณ แต่ OP ถามถึง "การซ่อน / การแรเงาของตัวแปรหรือฟังก์ชั่น" และคำว่า "parent", "child", "class" และ "member" ไม่ปรากฏในคำถามของเขา ดูเหมือนจะทำให้เป็นคำถามทั่วไปเกี่ยวกับขอบเขตชื่อ
John R. Strohm

3
@dylnmc ฉันไม่ได้คาดหวังว่าจะมีชีวิตอยู่นานพอที่จะพบกับวิปเปอร์สแนปเปอร์หนุ่มไม่พอที่จะได้รับการอ้างอิง "2001: A Space Odyssey" ที่ชัดเจน
John R. Strohm

15

มีใครรู้บ้างเกี่ยวกับการใช้แนวคิดเหล่านี้

การใช้ตัวระบุที่แม่นยำและสื่อความหมายนั้นเป็นประโยชน์อย่างมาก

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

การใช้สัญกรณ์ฮังการีบางอย่างเพื่อแยกแยะความแตกต่างของฟิลด์เทียบกับตัวแปรท้องถิ่นได้รับสิ่งนี้ แต่มีผลกระทบต่อการบำรุงรักษา (และสติปัญญาของโปรแกรมเมอร์)

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


3
ที่จริงแล้วไม่ใช่เรื่องง่ายที่จะใช้การซ่อนและการบังเงา เป็นเรื่องง่ายกว่าที่จะใช้ "ตัวแปรทั้งหมดเป็นสากล" คุณต้องการเนมสเปซเดียวเท่านั้นและคุณส่งออกชื่อเสมอเมื่อเทียบกับการมีเนมสเปซหลายรายการและต้องตัดสินใจว่าจะส่งออกหรือไม่
John R. Strohm

5
@ JohnR.Strohm - แน่นอน แต่ทันทีที่คุณมีการกำหนดขอบเขตใด ๆ (อ่าน: คลาส) แล้วให้ขอบเขตซ่อนขอบเขตที่ต่ำกว่าจะมีให้ฟรี
Telastyn

การกำหนดขอบเขตและคลาสเป็นสิ่งที่แตกต่างกัน ด้วยข้อยกเว้นของ BASIC ทุกภาษาที่ฉันตั้งโปรแกรมไว้มีการกำหนดขอบเขต แต่ไม่ใช่ทุกภาษาที่มีแนวคิดเกี่ยวกับคลาสหรือวัตถุใด ๆ
Michael Shaw

@michaelshaw - แน่นอนฉันควรจะชัดเจนมากขึ้น
Telastyn

7

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

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

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

public interface IFoo
{
   string MyFooString {get;}
   int FooMethod();
}

public class Foo:IFoo
{
   public string MyFooString {get{return "Foo";}}
   public int FooMethod() {//incredibly useful code here};
}

public class Bar:Foo
{
   //public new string MyFooString {get{return "Bar";}}
}

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

Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;

Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);

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

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

public class Bar:Foo
{
   public string MyBarString {get{return "Bar";}}       
}

ถูกกฎหมายอย่างสมบูรณ์แบบ แต่มันไม่ใช่พฤติกรรมที่เราต้องการ ตัวอย่างของ Bar จะผลิต "Foo" สำหรับทรัพย์สิน MyFooString เสมอเมื่อเราต้องการให้มันผลิต "Bar" ไม่เพียง แต่เราต้องรู้ว่า IFoo ของเราเป็นบาร์โดยเฉพาะเรายังต้องรู้จักการใช้ accessor ที่แตกต่างกัน

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

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   public int FooMethod() {...}
}

สำหรับตัวอย่างง่ายๆนี้มันเป็นคำตอบที่สมบูรณ์แบบตราบใดที่คุณใส่ใจว่า Foo และ Bar เป็นทั้ง IFoos โค้ดการใช้งานที่ตัวอย่างสองตัวอย่างไม่สามารถรวบรวมได้เนื่องจาก Bar ไม่ใช่ Foo และไม่สามารถกำหนดเช่นนี้ได้ อย่างไรก็ตามหาก Foo มีวิธีที่มีประโยชน์ "FooMethod" ที่แถบนั้นต้องการตอนนี้คุณจะไม่สามารถสืบทอดวิธีนั้นได้ คุณอาจต้องโคลนรหัสในบาร์หรือสร้างโฆษณา:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   private readonly theFoo = new Foo();

   public int FooMethod(){return theFoo.FooMethod();}
}

นี่คือการแฮ็กที่เห็นได้ชัดและในขณะที่การใช้ภาษา OO บางอย่างมีค่ามากกว่านี้ แต่แนวคิดก็ผิด หากผู้บริโภคของบาร์จำเป็นที่จะต้องเปิดเผยการทำงานของ Foo, บาร์ควรจะฟูไม่ได้มีฟู

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

public class Foo:IFoo
{
   public virtual string MyFooString {get{return "Foo";}}
   //...
}

public class Bar:Foo
{
   public override string MyFooString {get{return "Bar";}}
}

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

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


+1 สำหรับการตอบคำถามจริง ตัวอย่างที่ดีของการใช้งานการซ่อนสมาชิกแบบเรียลไทม์คือIEnumerableและIEnumerable<T>ส่วนต่อประสานที่อธิบายไว้ในบล็อกของ Eric Libbert ในหัวข้อ
Phil

การเอาชนะไม่ได้ซ่อน ฉันไม่เห็นด้วยกับ @Phil ว่าสิ่งนี้ตอบคำถาม
Jan Hudec

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

ฉันไม่ชอบการใช้แชโดว์ / ซ่อนของคุณ การใช้ประโยชน์หลัก ๆ ที่ฉันเห็นคือ (1) kludging เกี่ยวกับสถานการณ์ที่เวอร์ชันพื้นฐานของคลาสพื้นฐานมีสมาชิกที่ขัดแย้งกับรหัสผู้ใช้ที่ออกแบบมากับเวอร์ชั่นเก่า [น่าเกลียด แต่จำเป็น]; (2) การแกล้งทำสิ่งต่าง ๆ เช่นความแปรปรวนร่วมแบบกลับ (3) การจัดการกับกรณีที่วิธีการระดับฐานเป็นcallableในประเภทย่อยโดยเฉพาะอย่างยิ่ง แต่ไม่เป็นประโยชน์ LSP ต้องการอดีต แต่ไม่ต้องการหลังหากสัญญาระดับฐานระบุว่าวิธีการบางอย่างอาจไม่เป็นประโยชน์ในบางเงื่อนไข
supercat

2

Eric Lippert ผู้พัฒนาหลักในทีมคอมไพเลอร์ C # อธิบายได้ค่อนข้างดี (ขอบคุณ Lescai Ionel สำหรับลิงก์) .NET IEnumerableและIEnumerable<T>อินเทอร์เฟซเป็นตัวอย่างที่ดีเมื่อการซ่อนสมาชิกมีประโยชน์

ในยุคแรก ๆ ของ. NET เราไม่มียาชื่อสามัญ ดังนั้นIEnumerableอินเตอร์เฟสจะมีลักษณะดังนี้:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

อินเทอร์เฟซนี้เป็นสิ่งที่ทำให้เราสามารถforeachรวบรวมวัตถุได้อย่างไรก็ตามเราต้องใช้วัตถุเหล่านั้นเพื่อใช้งานอย่างถูกต้อง

จากนั้นยาชื่อสามัญมา เมื่อเราได้รับข้อมูลทั่วไปเรายังได้รับส่วนต่อประสานใหม่:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

ตอนนี้เราไม่จำเป็นต้องร่ายสิ่งของในขณะที่เราทำการวนซ้ำมัน! Woot! ตอนนี้ถ้าการซ่อนสมาชิกไม่ได้รับอนุญาตอินเทอร์เฟซจะต้องมีลักษณะดังนี้:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumeratorGeneric();
}

มันคงจะโง่แน่ ๆ เพราะ GetEnumerator()และGetEnumeratorGeneric()ในทั้งสองกรณีก็ทำสิ่งเดียวกันแต่พวกเขามีค่าตอบแทนที่ต่างกันเล็กน้อย ในความเป็นจริงแล้วคุณจะต้องการเริ่มต้นในรูปแบบทั่วไปGetEnumeratorเสมอเว้นแต่ว่าคุณกำลังทำงานกับรหัสดั้งเดิมที่เขียนก่อนที่ข้อมูลทั่วไปจะถูกนำไปใช้กับ. NET

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


ในขณะที่IEnumerable<T>.GetEnumerator()ซ่อนอย่างเป็นทางการIEnumerable.GetEnumerator()นี้เป็นเพียงเพราะ C # ไม่ได้มีประเภทกลับ covariant เมื่อเอาชนะ เหตุผลมันเป็นแทนที่อย่างเต็มที่สอดคล้องกับ LSP การซ่อนคือเมื่อคุณมีตัวแปรโลคัลmapในฟังก์ชันในไฟล์ที่ทำusing namespace std(ใน C ++)
Jan Hudec

2

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

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

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

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

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

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


2

นี่คือสองเซ็นต์ของฉัน

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

ชื่อที่บล็อกใช้แบ่งออกเป็นสามประเภท:

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

พิจารณาตัวอย่างโปรแกรม C ต่อไปนี้

#include<stdio.h>

void print_double_int(int n)
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4);
}

ฟังก์ชันprint_double_intมีชื่อโลคัล (ตัวแปรโลคัล) dและอาร์กิวเมนต์nและใช้ชื่อโกลบอลภายนอกprintfซึ่งอยู่ในขอบเขต แต่ไม่ได้กำหนดแบบโลคัล

โปรดสังเกตว่าprintfอาจถูกส่งผ่านเป็นอาร์กิวเมนต์:

#include<stdio.h>

void print_double_int(int n, int printf(const char *, ...))
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4, printf);
}

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

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

ยกตัวอย่างโค้ดสกาล่านี้:

object ClosureExample
{
  def createMultiplier(n: Int) = (m: Int) => m * n

  def main(args: Array[String])
  {
    val multiplier3 = createMultiplier(3)
    val multiplier5 = createMultiplier(5)

    // Prints 6.
    println(multiplier3(2))

    // Prints 10.
    println(multiplier5(2))
  }
}

ค่าตอบแทนของฟังก์ชั่นcreateMultiplierคือการปิด(m: Int) => m * nซึ่งมีข้อโต้แย้งและชื่อภายนอกm nชื่อที่nได้รับการแก้ไขโดยดูที่บริบทที่ปิดมีการกำหนดชื่อถูกผูกไว้กับข้อโต้แย้งของฟังก์ชั่นn createMultiplierโปรดสังเกตว่าการรวมนี้จะถูกสร้างขึ้นเมื่อมีการสร้างการปิดเช่นเมื่อcreateMultiplierถูกเรียกใช้ ดังนั้นชื่อnถูกผูกไว้กับค่าที่แท้จริงของการโต้แย้งสำหรับการเรียกใช้ฟังก์ชั่นเฉพาะ เปรียบเทียบสิ่งนี้กับกรณีของฟังก์ชั่นprintfไลบรารี่เช่นซึ่งแก้ไขโดยตัวเชื่อมโยงเมื่อสร้างไฟล์ปฏิบัติการได้

โดยสรุปมันจะมีประโยชน์ในการอ้างถึงชื่อภายนอกภายในบล็อกของรหัสเพื่อให้คุณ

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

Shadowing เกิดขึ้นเมื่อคุณพิจารณาว่าในบล็อกคุณสนใจเฉพาะชื่อที่เกี่ยวข้องที่กำหนดไว้ในสภาพแวดล้อมเช่นในprintfฟังก์ชั่นที่คุณต้องการใช้ ถ้าโดยโอกาสที่คุณต้องการที่จะใช้ชื่อท้องถิ่น ( getc, putc, scanf, ... ) ที่ได้ถูกนำมาใช้ในสภาพแวดล้อมที่คุณต้องการง่ายที่จะไม่สนใจ (เงา) ชื่อทั่วโลก ดังนั้นเมื่อคิดในพื้นที่คุณไม่ต้องการพิจารณาบริบททั้งหมด (อาจใหญ่มาก)

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

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

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