ภาษาที่มีประเภทบางทีแทนที่จะเป็นค่าศูนย์จะจัดการกับเงื่อนไขของขอบได้อย่างไร


53

เอริค Lippert ทำให้เป็นจุดที่น่าสนใจมากในการอภิปรายของเขาว่าทำไม C # ใช้nullมากกว่าMaybe<T>ประเภท :

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

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


ฉันเดาว่าอาจจะเป็นส่วนหนึ่งของภาษามันอาจเป็นไปได้ว่ามันมีการใช้งานภายในผ่านตัวชี้โมฆะและมันเป็นเพียงน้ำตาล syntactic แต่ฉันไม่คิดว่าภาษาใดที่จะทำอย่างนี้
panzi

1
@panzi: Ceylon ใช้การพิมพ์ที่ไวต่อการไหลเพื่อแยกความแตกต่างระหว่างType?(อาจ) และType(ไม่ใช่ null)
Lukas Eder

1
@RobertHarvey ไม่มีปุ่ม "คำถามที่ดี" ใน Stack Exchange แล้วใช่ไหม
user253751

2
@panzi นั่นเป็นการเพิ่มประสิทธิภาพที่ดีและถูกต้อง แต่ก็ไม่ได้ช่วยแก้ไขปัญหานี้: เมื่อมีบางอย่างไม่ใช่Maybe Tมันจะต้องไม่เป็นNoneเช่นนั้นคุณจึงไม่สามารถเริ่มต้นการจัดเก็บข้อมูลของตัวชี้นั้นได้

@immibis: ฉันผลักมันไปแล้ว เราได้รับคำถามที่ดีที่มีค่าไม่กี่ที่นี่; ฉันคิดว่าอันนี้สมควรได้รับความคิดเห็น
Robert Harvey

คำตอบ:


45

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

class Broken {
    val foo: Foo  // where Foo and Bar are non-nullable reference types
    val bar: Bar

    Broken() {
        foo = new Foo()
        throw new Exception()
        // this code is never reached, so "bar" is not assigned
        bar = new Bar()
    }

    ~Broken() {
        foo.cleanup()
        bar.cleanup()
    }
}

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

ด้วย nulls, destructor if (foo != null) foo.cleanup()จะทดสอบว่าตัวแปรที่ได้รับมอบหมายเช่น หากไม่มี nulls วัตถุจะอยู่ในสถานะไม่ได้กำหนดค่าของbarคืออะไร?

อย่างไรก็ตามปัญหานี้มีอยู่เนื่องจากการรวมกันของสามด้าน:

  • ไม่มีค่าเริ่มต้นเช่นnullหรือรับประกันการเริ่มต้นสำหรับตัวแปรสมาชิก
  • ความแตกต่างระหว่างการประกาศและการมอบหมาย การบังคับให้ตัวแปรถูกกำหนดทันที (เช่นด้วยletคำสั่งตามที่เห็นในภาษาที่ใช้งานได้) นั้นเป็นเรื่องง่ายที่จะบังคับให้เริ่มต้นรับประกัน - แต่ จำกัด ภาษาในรูปแบบอื่น
  • รสชาติเฉพาะของ destructors เป็นวิธีการที่เรียกใช้โดยรันไทม์ภาษา

เป็นการง่ายที่จะเลือกการออกแบบอื่นที่ไม่แสดงปัญหาเหล่านี้ตัวอย่างเช่นการรวมการประกาศด้วยการมอบหมายและการให้ภาษาเสนอบล็อก finalizer หลายบล็อกแทนที่จะเป็นวิธีการสรุปเดียว:

// the body of the class *is* the constructor
class Working() {
    val foo: Foo = new Foo()
    FINALIZE { foo.cleanup() }  // block is registered to run when object is destroyed

    throw new Exception()

    // the below code is never reached, so
    //  1. the "bar" variable never enters the scope
    //  2. the second finalizer block is never registered.
    val bar: Bar = new Bar()
    FINALIZE { bar.cleanup() }  // block is registered to run when object is destroyed
}

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

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


นอกจากนี้ยังมีอีกเหตุผลว่าทำไม finalizer ต้องจัดการกับnulls: ลำดับของการสรุปไม่รับประกันเนื่องจากความเป็นไปได้ของรอบอ้างอิง แต่ฉันเดาว่าFINALIZEการออกแบบของคุณยังแก้ปัญหาได้: หากfooได้รับการสรุปแล้วFINALIZEส่วนของมันจะไม่ทำงาน
svick

14

วิธีเดียวกับที่คุณรับประกันข้อมูลอื่น ๆ ที่อยู่ในสถานะที่ถูกต้อง

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

ยกตัวอย่างเช่นใน Rust คุณสร้างวัตถุชนิด struct ผ่านทางแทนการเขียนสร้างที่ไม่ได้Point { x: 1, y: 2 } self.x = 1; self.y = 2;แน่นอนว่านี่อาจขัดแย้งกับรูปแบบของภาษาที่คุณนึกไว้

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

Object o;
try {
    call_can_throw();
    o = new Object();
} catch {}
use(o);

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


7

นี่คือวิธีที่ Haskell ทำ: (ไม่ใช่สิ่งที่ตรงกันข้ามกับข้อความของ Lippert เนื่องจาก Haskell ไม่ใช่ภาษาเชิงวัตถุ)

คำเตือน: คำตอบที่ยืดยาวจากแฟนบอย Haskell ข้างหน้า

TL; DR

ตัวอย่างนี้แสดงให้เห็นว่า Haskell แตกต่างจาก C # อย่างไร แทนที่จะมอบหมายการขนส่งของการก่อสร้างโครงสร้างให้กับตัวสร้างมันจะต้องได้รับการจัดการในรหัสโดยรอบ ไม่มีวิธีใดสำหรับค่า null (หรือNothingใน Haskell) เพื่อครอบตัดค่าที่เราคาดหวังว่าค่าที่ไม่ใช่ค่า null เนื่องจากค่า null สามารถเกิดขึ้นได้เฉพาะในประเภท wrapper พิเศษที่เรียกว่าMaybeซึ่งไม่สามารถใช้แทน / แปลงได้โดยตรงเป็นปกติไม่ใช่ ประเภท nullable เพื่อที่จะใช้ค่าที่ถูกทำให้เป็นโมฆะโดยการห่อใน a Maybe, ก่อนอื่นเราต้องแยกค่าโดยใช้การจับคู่รูปแบบซึ่งบังคับให้เราเบี่ยงเบนการควบคุมการไหลเข้าไปในสาขาที่เรารู้แน่นอนว่าเรามีค่าที่ไม่เป็นโมฆะ

ดังนั้น:

เราจะรู้ได้หรือไม่ว่าการอ้างอิงที่ไม่เป็นโมฆะนั้นไม่เคยภายใต้สถานการณ์ใด ๆ

ใช่. IntและMaybe Intเป็นสองประเภทที่แยกจากกันอย่างสมบูรณ์ หาNothingในธรรมดาIntจะเปรียบได้กับการหาสตริง "ปลา" Int32ใน

สิ่งที่เกี่ยวกับในตัวสร้างของวัตถุที่มีเขตข้อมูลที่ไม่เป็นโมฆะประเภทการอ้างอิง?

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

สิ่งที่เกี่ยวกับใน finalizer ของวัตถุดังกล่าวที่วัตถุจะเสร็จสมบูรณ์เพราะรหัสที่ควรจะกรอกในการอ้างอิงโยนข้อยกเว้น?

ไม่มีผู้ผ่านเข้ารอบสุดท้ายใน Haskell ดังนั้นฉันจึงไม่สามารถพูดเรื่องนี้ได้ อย่างไรก็ตามคำตอบแรกของฉันยังคงมีอยู่

คำตอบแบบเต็ม :

Haskell ไม่มีค่าว่างและใช้Maybeชนิดข้อมูลเพื่อแสดงค่าว่าง อาจเป็นชนิดข้อมูล algabraic ที่กำหนดเช่นนี้:

data Maybe a = Just a | Nothing

สำหรับคนที่ไม่คุ้นเคยกับ Haskell ให้อ่านสิ่งนี้ว่า "A Maybeis a a Nothingor a Just a" โดยเฉพาะ:

  • Maybeเป็นตัวสร้างประเภท : มันสามารถคิด (ไม่ถูกต้อง) เป็นคลาสทั่วไป (ซึ่งaเป็นตัวแปรประเภท) C # class Maybe<a>{}เปรียบเทียบคือ
  • Justเป็นตัวสร้างค่า : มันเป็นฟังก์ชั่นที่รับอาร์กิวเมนต์ชนิดหนึ่งaและคืนค่าประเภทMaybe aที่มีค่า ดังนั้นรหัสจะคล้ายคลึงกับx = Just 17int? x = 17;
  • Nothingเป็นตัวสร้างค่าอื่น แต่ไม่รับอาร์กิวเมนต์และค่าMaybeส่งคืนจะไม่มีค่าอื่นใดนอกจาก "ไม่มี" x = Nothingคล้ายกับint? x = null;(สมมติว่าเราถูก จำกัดaใน Haskell Intซึ่งสามารถทำได้โดยการเขียนx = Nothing :: Maybe Int)

เมื่อพื้นฐานของMaybeประเภทนั้นหมดไปแล้ว Haskell จะหลีกเลี่ยงปัญหาที่กล่าวถึงในคำถามของ OP ได้อย่างไร

ดี Haskell เป็นจริงที่แตกต่างจากส่วนใหญ่ของภาษาที่กล่าวถึงเพื่อให้ห่างไกลดังนั้นฉันจะเริ่มต้นด้วยการอธิบายหลักการใช้ภาษาไม่กี่ขั้นพื้นฐาน

ออกครั้งแรกใน Haskell, ทุกอย่างจะไม่เปลี่ยนรูป ทุกอย่าง ชื่ออ้างถึงค่าไม่ใช่ตำแหน่งหน่วยความจำที่สามารถเก็บค่าได้ (เพียงอย่างเดียวนี้เป็นแหล่งกำจัดข้อผิดพลาดอย่างใหญ่หลวง) ซึ่งแตกต่างจากใน C # ที่ประกาศตัวแปรและการมอบหมายเป็นสองการดำเนินงานแยกต่างหากในค่า Haskell จะถูกสร้างขึ้นโดยการกำหนดค่าของพวกเขา (เช่นx = 15, y = "quux", z = Nothing) ซึ่งไม่สามารถเปลี่ยนแปลง ดังนั้นรหัสเช่น:

ReferenceType x;

เป็นไปไม่ได้ใน Haskell ไม่มีปัญหาในการเตรียมใช้งานค่าnullเนื่องจากทุกอย่างต้องถูกเตรียมใช้งานอย่างชัดเจนเป็นค่าเพื่อให้มีอยู่

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

ถัดไปไม่มีรหัสลักษณะที่แน่นอน โดยสิ่งนี้ฉันหมายความว่าภาษาส่วนใหญ่มีรูปแบบดังนี้:

do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
    do thing 6
return thing 7

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

ใน Haskell นี่เป็นไปไม่ได้ แต่โฟลว์ของโปรแกรมจะถูกกำหนดโดยฟังก์ชันการโยง แม้แต่คำอธิบายที่ดูเป็นเรื่องจำเป็นdoก็แค่น้ำตาลเชิงประโยคสำหรับส่งผ่านฟังก์ชั่นนิรนามไปยัง>>=ผู้ปฏิบัติงาน ฟังก์ชั่นทั้งหมดอยู่ในรูปแบบของ:

<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression

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

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

ด้วยสิ่งเหล่านี้ในใจลองดูตัวอย่างของ "แตกหัก" ของอมรและลองแสดงรหัสนี้อีกครั้งใน Haskell ก่อนการประกาศข้อมูล (ใช้ไวยากรณ์บันทึกสำหรับเขตข้อมูลที่มีชื่อ):

data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar } 

( fooและbarเป็นฟังก์ชั่นการเข้าถึงไปยังฟิลด์ที่ไม่ระบุตัวตนที่นี่แทนที่จะเป็นฟิลด์จริง แต่เราสามารถละเว้นรายละเอียดนี้ได้)

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

ในตัวอย่างการสร้างBrokenล้มเหลวเสมอ ไม่มีวิธีใดที่จะทำลายตัวNotSoBrokenสร้างค่าในลักษณะที่คล้ายกัน (ไม่มีที่ไหนเลยที่จะเขียนโค้ด) แต่เราสามารถสร้างฟังก์ชั่นจากโรงงานที่มีข้อบกพร่องในทำนองเดียวกัน

makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing

(บรรทัดแรกคือการประกาศลายเซ็นประเภท: makeNotSoBrokenใช้เวลาFooและBarเป็นอาร์กิวเมนต์และสร้างกMaybe NotSoBroken)

ประเภทผลตอบแทนต้องMaybe NotSoBrokenและไม่เพียงNotSoBrokenเพราะเราบอกว่ามันจะประเมินซึ่งเป็นตัวสร้างความคุ้มค่าNothing Maybeประเภทจะไม่เข้าแถวถ้าเราเขียนอะไรที่ต่างออกไป

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

useNotSoBroken :: NotSoBroken -> Whatever

( useNotSoBrokenยอมรับ a NotSoBrokenเป็นอาร์กิวเมนต์และสร้าง a Whatever)

และใช้มันอย่างนั้น:

useNotSoBroken (makeNotSoBroken)

ในภาษาส่วนใหญ่พฤติกรรมประเภทนี้อาจทำให้เกิดข้อยกเว้นตัวชี้โมฆะ ใน Haskell ประเภทไม่ตรงกับขึ้นmakeNotSoBrokenส่งกลับMaybe NotSoBrokenแต่คาดว่าจะมีuseNotSoBroken NotSoBrokenประเภทเหล่านี้ไม่สามารถใช้แทนกันได้และรหัสไม่สามารถคอมไพล์ได้

ในการหลีกเลี่ยงปัญหานี้เราสามารถใช้caseคำสั่งแยกสาขาตามโครงสร้างของMaybeค่า (ใช้คุณลักษณะที่เรียกว่าการจับคู่รูปแบบ ):

case makeNotSoBroken of
    Nothing  -> --handle situation here
    (Just x) -> useNotSoBroken x

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

  • ครั้งแรกที่ได้รับการประเมินซึ่งรับประกันได้ว่าจะก่อให้เกิดค่าของชนิดmakeNotSoBrokenMaybe NotSoBroken
  • caseคำสั่งตรวจสอบโครงสร้างของค่านี้
  • หากมีค่ารหัสNothing"จัดการสถานการณ์ที่นี่" จะถูกประเมิน
  • หากค่านั้นตรงกับค่าแทนJustจะมีการดำเนินการกับสาขาอื่น ให้สังเกตว่าประโยคที่ตรงกันนั้นระบุค่าเป็นJustโครงสร้างและผูกNotSoBrokenฟิลด์ภายในกับชื่ออย่างไร (ในกรณีนี้x) xสามารถใช้เช่นเดียวกับNotSoBrokenค่าปกติที่เป็น

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

ฉันหวังว่านี่เป็นคำอธิบายที่เข้าใจยาก ถ้ามันไม่เข้าท่าให้กระโดดเข้าไปในLearn You A Haskell for Great Good! หนึ่งในบทเรียนภาษาออนไลน์ที่ดีที่สุดที่ฉันเคยอ่าน หวังว่าคุณจะเห็นความงามแบบเดียวกันในภาษานี้ที่ฉันทำ


TL; DR ควรอยู่ด้านบน :)
andrew.fox

@ andrew.fox จุดที่ดี ฉันจะแก้ไข
ApproachDarknessFish

0

ฉันคิดว่าคำพูดของคุณเป็นเรื่องฟางคน

ภาษาสมัยใหม่ทุกวันนี้ (รวมถึง C #) รับประกันได้ว่าตัวสร้างจะเสร็จสมบูรณ์หรือไม่

หากมีข้อยกเว้นในตัวสร้างและวัตถุถูกปล่อยให้บางส่วนไม่ได้เตรียมการบางส่วนการมีnullหรือMaybe::noneสำหรับสถานะที่ไม่ได้กำหนดค่าเริ่มต้นนั้นไม่ทำให้เกิดความแตกต่างที่แท้จริงในรหัส destructor

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

Btw: ใน C # มูลค่าเทียบเท่าสวยมากnull Maybe::noneคุณสามารถกำหนดให้nullกับตัวแปรและสมาชิกออบเจกต์ที่มีการประกาศระดับประเภทเป็นโมฆะเท่านั้น :

String? nullableString = getOptionalString();
Nullable<String> maybe = nullableString; // This is equivalent

สิ่งนี้ไม่แตกต่างจากตัวอย่างต่อไปนี้:

Maybe<String> optionalString = getOptionalString();

ดังนั้นโดยสรุปฉันไม่เห็นว่าความเป็นโมฆะในทางตรงกันข้ามกับMaybeประเภทใด ฉันก็จะแนะนำว่า C # ได้แอบอยู่ในนั้นของตัวเองชนิดและเรียกมันว่าMaybeNullable<T>

ด้วยวิธีการขยายมันเป็นเรื่องง่ายที่จะทำความสะอาดNullableให้เป็นไปตามรูปแบบ monadic:

Resource? resource = initializationThatMayFail();
...
resource.ifExists( Resource r -> r.cleanup() );

2
มันหมายความว่าอะไร "คอนสตรัคเตอร์เสร็จสมบูรณ์อย่างสมบูรณ์หรือไม่"? ใน Java ตัวอย่างเช่นการเริ่มต้นของเขตข้อมูล (ไม่ใช่ครั้งสุดท้าย) ในตัวสร้างไม่ได้รับการคุ้มครองจากการแข่งขันข้อมูล - ไม่ว่ามีคุณสมบัติครบถ้วนสมบูรณ์หรือไม่?
ริ้น

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

ความแตกต่างใหญ่ระหว่างnullสมาชิกเป็นนัยของทุกชนิดและMaybe<T>เป็นความประสงค์ว่าMaybe<T>คุณยังสามารถมีเพียงTซึ่งไม่ได้มีค่าเริ่มต้นใด ๆ
svick

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

@svick: ใน C # (ซึ่งเป็นภาษาที่เป็นปัญหาโดย OP) nullไม่ได้เป็นสมาชิกโดยนัยของทุกประเภท สำหรับการnullที่จะเป็นค่า lebal คุณจะต้องกำหนดประเภทที่จะ nullable อย่างชัดเจนซึ่งจะทำให้T?(น้ำตาลไวยากรณ์สำหรับNullable<T>) Maybe<T>เป็นหลักเทียบเท่ากับ
Roland Tepp

-3

C ++ ทำได้โดยการเข้าถึง initializer ที่เกิดขึ้นก่อนเนื้อความ Constructor C # รัน initializer เริ่มต้นก่อนเนื้อความคอนสตรัคมันประมาณ 0 ให้กับทุกอย่างfloatsกลายเป็น 0.0 boolsกลายเป็นเท็จการอ้างอิงกลายเป็นโมฆะ ฯลฯ ใน C ++ คุณสามารถทำให้มันรัน initializer ที่แตกต่างกันเพื่อให้แน่ใจว่าประเภทการอ้างอิงที่ไม่เป็นโมฆะ .

class Foo { Foo(int i) { throw new Exception("Never finishes"); }
class Bar { Bar(string s) { } }

class Broken
{
    val foo: Foo  // where Foo and Bar are non-nullable reference types
    val bar: Bar

    Broken() :
        foo = new Foo(123),// roughly causes a "goto destroy_foo;"
        bar = new Bar("never executes") { }

    // This destructory-function never runs because the constructor never completed
    ~Broken() 
    // This is made-up syntax:
    // : 
    // destroy_bar:
    // bar.~Bar();
    // destroy_foo:
    // foo.~Foo();
    {
    }
}

2
คำถามเกี่ยวกับภาษาที่มีประเภทบางที
ริ้น

3
การอ้างอิงกลายเป็นโมฆะ ” - หลักฐานทั้งหมดของคำถามคือเราไม่มีnullและวิธีเดียวที่จะระบุว่าไม่มีค่าคือการใช้Maybeประเภท (หรือที่รู้จักกันในชื่อOption) ซึ่ง AFAIK C ++ ไม่มีอยู่ใน ห้องสมุดมาตรฐาน การขาดงานของ nulls ช่วยให้เราสามารถรับประกันได้ว่าสนามจะเป็นที่ถูกต้องถือเป็นทรัพย์สินของระบบการพิมพ์ที่ nullนี่คือการรับประกันที่แข็งแกร่งกว่าด้วยตนเองเพื่อให้แน่ใจว่าเส้นทางรหัสที่ไม่มีตัวแปรยังอาจจะมี
amon

ในขณะที่ c ++ ไม่ได้มีประเภทบางทีอย่างชัดเจนสิ่งต่าง ๆ เช่น std :: shared_ptr <T> อยู่ใกล้พอที่ฉันคิดว่ามันยังคงมีความเกี่ยวข้องที่ c ++ จัดการกับกรณีที่การเริ่มต้นของตัวแปรสามารถเกิดขึ้นได้ "นอกขอบเขต" ของตัวสร้างและ ในความเป็นจริงเป็นสิ่งจำเป็นสำหรับประเภทอ้างอิง (&) เนื่องจากไม่สามารถเป็นโมฆะได้
FryGuy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.