ฟังก์ชั่นมากไปตามประเภทการคืนสินค้า?


252

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


คำตอบ:


523

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

int func();
string func();
int main() { func(); }

คุณไม่สามารถบอกfunc()ได้ว่ากำลังเรียกใคร วิธีนี้สามารถแก้ไขได้ในสองสามวิธี:

  1. มีวิธีการที่คาดเดาได้เพื่อกำหนดฟังก์ชั่นที่เรียกว่าในสถานการณ์ดังกล่าว
  2. เมื่อใดก็ตามที่สถานการณ์ดังกล่าวเกิดขึ้นจะเป็นข้อผิดพลาดในการรวบรวมเวลา int main() { (string)func(); }อย่างไรก็ตามมีไวยากรณ์ที่ช่วยให้โปรแกรมเมอร์ที่จะกระจ่างเช่น
  3. ไม่มีผลข้างเคียง หากคุณไม่มีผลข้างเคียงและไม่เคยใช้ค่าส่งคืนของฟังก์ชันคอมไพเลอร์สามารถหลีกเลี่ยงการเรียกใช้ฟังก์ชันได้ตั้งแต่แรก

สองภาษาที่ฉันเป็นประจำ ( AB ) การใช้งานเกินพิกัดตามประเภทผลตอบแทน: PerlและHaskell ให้ฉันอธิบายสิ่งที่พวกเขาทำ

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

print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.

ผู้ประกอบการทุกคนใน Perl ทำอะไรบางอย่างในบริบทเซนต์คิตส์และในบริบทรายการและพวกเขาอาจแตกต่างกันตามที่แสดง (นี่ไม่ได้มีไว้สำหรับตัวดำเนินการสุ่มlocaltimeเท่านั้นเช่นถ้าคุณใช้อาร์เรย์@aในบริบทรายการมันจะส่งกลับอาร์เรย์ในขณะที่อยู่ในบริบทสเกลามันจะส่งกลับจำนวนองค์ประกอบดังนั้นตัวอย่างเช่นprint @aพิมพ์องค์ประกอบออกมาในขณะที่print 0+@aพิมพ์ขนาด ) นอกจากนี้ผู้ประกอบการทุกคนสามารถบังคับบริบทเช่นการเพิ่ม+กองกำลังบริบทสเกลาร์ ทุกรายการในman perlfuncเอกสารนี้ ตัวอย่างเช่นนี่เป็นส่วนหนึ่งของรายการสำหรับglob EXPR:

ในบริบทรายการส่งกลับรายการ (อาจจะเป็นที่ว่างเปล่า) การขยายชื่อไฟล์กับค่าของEXPRเช่น Unix มาตรฐานเปลือก/bin/cshจะทำ ในบริบทสเกลาร์, glob ซ้ำผ่านการขยายชื่อไฟล์ดังกล่าวกลับ undef เมื่อรายการหมด

ทีนี้ความสัมพันธ์ระหว่างรายการกับบริบทสเกลาร์คืออะไร เอาละman perlfuncพูดว่า

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

ดังนั้นจึงไม่ใช่เรื่องง่ายที่จะมีฟังก์ชั่นเดียวจากนั้นคุณก็ทำการแปลงอย่างง่ายในตอนท้าย อันที่จริงฉันเลือกlocaltimeตัวอย่างด้วยเหตุผลนั้น

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

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

sub func {
    if( not defined wantarray ) {
        print "void\n";
    } elsif( wantarray ) {
        print "list\n";
    } else {
        print "scalar\n";
    }
}

func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"

(หมายเหตุ: บางครั้งฉันอาจพูดว่าผู้ประกอบการ Perl เมื่อฉันหมายถึงฟังก์ชั่นนี้ไม่สำคัญกับการสนทนานี้)

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

main = do n <- readLn
          print (sqrt n) -- note that this is aligned below the n, if you care to run this

รหัสนี้อ่านตัวเลขทศนิยมจากอินพุตมาตรฐานและพิมพ์สแควร์รูท แต่สิ่งที่น่าแปลกใจเกี่ยวกับเรื่องนี้คืออะไร? ดีประเภทของการเป็นreadLn readLn :: Read a => IO aสิ่งนี้หมายความว่าสำหรับประเภทใด ๆ ที่สามารถRead(อย่างเป็นทางการทุกประเภทที่เป็นตัวอย่างของReadคลาสประเภท) readLnสามารถอ่านได้ Haskell รู้ได้อย่างไรว่าฉันต้องการอ่านเลขทศนิยม ชนิดของsqrtคือsqrt :: Floating a => a -> aซึ่งโดยพื้นฐานแล้วหมายความว่าsqrtสามารถยอมรับเฉพาะตัวเลขจุดลอยตัวเป็นอินพุตเท่านั้นดังนั้น Haskell จึงสรุปสิ่งที่ฉันต้องการ

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

main = do n <- readLn
          print n
-- this program results in a compile-time error "Unresolved top-level overloading"

ฉันสามารถแก้ไขความกำกวมโดยการระบุประเภทที่ฉันต้องการ:

main = do n <- readLn
          print (n::Int)
-- this compiles (and does what I want)

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

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

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

C ++ (ส่วนย่อย 7.4.1 จาก Bjarne Stroustrup ของ "ภาษาการเขียนโปรแกรม C ++"): "ประเภทการส่งคืนจะไม่ได้รับการพิจารณาในการแก้ปัญหาการโอเวอร์โหลดเหตุผลคือเพื่อให้การแก้ไขสำหรับผู้ประกอบการแต่ละรายหรือการเรียกใช้ฟังก์ชัน

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
    float fl = sqrt(da);     // call sqrt(double)
    double d = sqrt(da); // call sqrt(double)
    fl = sqrt(fla);            // call sqrt(float)
    d = sqrt(fla);             // call sqrt(float)
}

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

Java ( Java Language Specification 9.4.1 ): "หนึ่งในวิธีการที่สืบทอดมานั้นจะต้องเป็น return-type-substitutable ทุก ๆ วิธีที่สืบทอดมาอื่น ๆ หรือมิฉะนั้นจะเกิดข้อผิดพลาดในการรวบรวมเวลา" (ใช่ฉันรู้ว่านี่ไม่ได้ให้เหตุผลฉันแน่ใจว่าเหตุผลที่ได้รับจาก Gosling ใน "ภาษาการเขียนโปรแกรมภาษาจาวา" บางทีอาจมีบางคนมีสำเนาฉันคิดว่ามันเป็น "หลักการของความประหลาดใจอย่างน้อย" ) อย่างไรก็ตามข้อเท็จจริงที่สนุกสนานเกี่ยวกับ Java: JVM อนุญาตให้โหลดเกินโดยส่งคืนค่า! ตัวอย่างนี้ใช้ในScalaและสามารถเข้าถึงได้โดยตรงผ่าน Javaเช่นกันโดยการเล่นกับผู้เล่นภายใน

PS ในฐานะโน้ตสุดท้ายเป็นไปได้จริงที่จะโอเวอร์โหลดโดยส่งคืนค่าใน C ++ พร้อมกับเล่ห์เหลี่ยม พยาน:

struct func {
    operator string() { return "1";}
    operator int() { return 2; }
};

int main( ) {
    int x    = func(); // calls int version
    string y = func(); // calls string version
    double d = func(); // calls int version
    cout << func() << endl; // calls int version
    func(); // calls neither
}

โพสต์ที่ยอดเยี่ยม แต่คุณอาจต้องการชี้แจงว่าการอ่านคืออะไร (สตริง -> บางอย่าง)
Thomas Eding

C ++ ยังให้คุณโอเวอร์โหลดโดย const / ไม่ใช่ค่าที่ส่งคืนของ const stackoverflow.com/questions/251159/…
geon

3
สำหรับเคล็ดลับสุดท้ายของคุณที่มีการใช้งานโอเปอเรเตอร์มากเกินไปบางครั้งบรรทัด "cout" จะใช้งานได้ แต่บางครั้งการเปลี่ยนแปลงใด ๆ ที่ฉันทำกับโค้ดทำให้มันเป็น "โอเวอร์โหลดที่คลุมเครือสำหรับ 'โอเปอเรเตอร์ <<'"
Steve

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

... วิธีการที่สามารถกำหนดประเภทที่ควรใช้ในการสร้างเช่นvar someVar = someMethod();(หรืออื่น ๆ ที่ระบุว่าการส่งคืนของมันไม่ควรใช้ในรูปแบบดังกล่าว) ตัวอย่างเช่นตระกูลประเภทที่ใช้อินเทอร์เฟซ Fluent อาจได้รับประโยชน์จากการมีเวอร์ชันที่ไม่แน่นอนและไม่เปลี่ยนรูปดังนั้นvar thing2 = thing1.WithX(3).WithY(5).WithZ(9);จะมีการWithX(3)คัดลอกthing1ไปยังวัตถุที่ไม่แน่นอน, กลายพันธุ์ X และส่งคืนวัตถุที่ไม่แน่นอน WithY(5)จะกลายพันธุ์ Y และกลับวัตถุเดียวกัน; เช่นเดียวกัน `WithZ (9) จากนั้นการมอบหมายจะแปลงเป็นประเภทที่ไม่เปลี่ยนรูป
supercat

37

หากฟังก์ชั่นโอเวอร์โหลดตามประเภทของการส่งคืนและคุณมีโอเวอร์โหลดสองตัวนี้

int func();
string func();

ไม่มีทางที่คอมไพเลอร์สามารถคิดได้ว่าจะเรียกฟังก์ชั่นใดของทั้งสองฟังก์ชั่นนี้

void main() 
{
    func();
}

ด้วยเหตุนี้นักออกแบบภาษาจึงไม่อนุญาตให้โหลดเกินค่าที่ส่งคืน

บางภาษา (เช่น MSIL) แต่ไม่อนุญาตให้มีการบรรทุกเกินพิกัดตามประเภทของผลตอบแทน พวกเขาเผชิญกับความยากลำบากดังกล่าวข้างต้นแน่นอน แต่พวกเขามีวิธีแก้ไขปัญหาที่คุณจะต้องศึกษาเอกสารของพวกเขา


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

2
@ Jörg W Mittag: คุณไม่เห็นว่าฟังก์ชั่นทำอะไร พวกเขาสามารถมีผลข้างเคียงที่แตกต่างกันได้อย่างง่ายดาย
A. Rex

2
@ Jörg - ในภาษาโปรแกรมกระแสหลักส่วนใหญ่ (C / C ++, C #, Java, ฯลฯ ) ฟังก์ชั่นโดยทั่วไปมีผลข้างเคียง อันที่จริงฉันเดาว่าฟังก์ชั่นที่มีผลข้างเคียงอย่างน้อยก็เหมือนกับคนที่ไม่มี
ไมเคิลเสี้ยน

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

3
การกระโดดเข้ามาในภายหลังมุมมองบางอย่างอาจใช้คำคุณศัพท์อื่น ๆ ที่นอกเหนือจากที่เข้มงวดหรืออวดความรู้
Patrick McDonald

27

ในภาษาดังกล่าวคุณจะแก้ไขปัญหาต่อไปนี้อย่างไร:

f(g(x))

หากfมีมากเกินไปvoid f(int)และvoid f(string)และgมีมากเกินไปint g(int)และstring g(int)? คุณจะต้องมี disambiguator บ้าง

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


2
การบรรทุกเกินพิกัดแบบปกติอาจส่งผลให้เกิดความกำกวมได้เช่นกัน ฉันคิดว่าสิ่งเหล่านี้ได้รับการแก้ไขตามปกติโดยนับจำนวนการปลดเปลื้องที่จำเป็น แต่วิธีนี้ไม่ได้ผลเสมอไป
464142 Jay Conrod

1
ใช่การแปลงมาตรฐานได้รับการจัดอันดับในการจับคู่การส่งเสริมการขายและการแปลงที่แน่นอน: void f (int); โมฆะ f (ยาว); f ( 'a'); เรียก f (int) เพราะเป็นเพียงการส่งเสริมการขายในขณะที่การแปลงเป็นความยาวเป็นการแปลง เป็นโมฆะ f (ลอย); เป็นโมฆะ f (สั้น); f (10); จะต้องมีการแปลงสำหรับทั้งสอง: การโทรไม่ชัดเจน
Johannes Schaub - litb

หากภาษามีการประเมินผลที่ขี้เกียจนี่ไม่ใช่ปัญหามากนัก
jdd

upvote การทำงานร่วมกันของพารามิเตอร์การบรรทุกเกินพิกัดและการส่งคืนการบรรทุกเกินพิกัดจะไม่ได้รับการแก้ไขในโพสต์ของ Rex จุดที่ดีมาก
โจเซฟการ์วิน

1
ถ้าฉันออกแบบภาษากฎของฉันก็คือว่าสำหรับฟังก์ชั่นโอเวอร์โหลดใด ๆ ลายเซ็นของพารามิเตอร์แต่ละตัวจะต้องมีหนึ่งประเภทผลตอบแทนที่กำหนดเป็นค่าเริ่มต้น คอมไพเลอร์จะเริ่มต้นโดยสมมติว่าการเรียกใช้ฟังก์ชันทุกครั้งจะใช้ประเภทเริ่มต้น เมื่อทำเช่นนั้นแล้วอย่างไรก็ตามในทุกสถานการณ์ที่ค่าส่งคืนของฟังก์ชันถูกส่งออกไปหรือบีบบังคับกับสิ่งอื่นทันทีคอมไพเลอร์จะตรวจสอบโอเวอร์โหลดซึ่งพารามิเตอร์ของลายเซ็นเหมือนกัน แต่มีประเภทการคืนที่ตรงกันดีกว่า . ฉันอาจจะกำหนดกฎ "override-one - override-all" ทั้งหมดสำหรับโอเวอร์โหลดดังกล่าว
supercat

19

หากต้องการขโมยคำตอบเฉพาะ C ++ จากคำถามอื่นที่คล้ายกัน (dupe?):


ประเภทการคืนฟังก์ชั่นไม่ได้มาจากการเล่นในความละเอียดเกินพิกัดเพียงเพราะ Stroustrup (ฉันสมมติว่ามีอินพุตจากสถาปนิก C ++ คนอื่น ๆ ) ต้องการให้ความละเอียดเกินพิกัดเป็น 'บริบทที่เป็นอิสระ' ดู 7.4.1 - "การโอเวอร์โหลดและประเภทการส่งคืน" จาก "ภาษาการเขียนโปรแกรม C ++ รุ่นที่สาม"

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

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

และลอร์ดรู้ดีว่าการแก้ปัญหาการโอเวอร์โหลด C ++ นั้นซับซ้อนพอที่จะเป็น ...


5

ใน Haskell เป็นไปได้แม้ว่ามันจะไม่มีฟังก์ชั่นมากไป Haskell ใช้คลาสประเภท ในโปรแกรมคุณสามารถดู:

class Example a where
    example :: Integer -> a

instance Example Integer where  -- example is now implemented for Integer
    example :: Integer -> Integer
    example i = i * 10

ฟังก์ชั่นการโอเวอร์โหลดตัวเองไม่เป็นที่นิยม ภาษาส่วนใหญ่ที่ฉันเคยเห็นคือ C ++ บางที java และ / หรือ C # ในภาษาที่มีพลวัตทั้งหมดมันเป็นสิ่งที่จดจำสำหรับ:

define example:i
  ↑i type route:
    Integer = [↑i & 0xff]
    String = [↑i upper]


def example(i):
    if isinstance(i, int):
        return i & 0xff
    elif isinstance(i, str):
        return i.upper()

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

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

คุณจะเห็นว่ามีฟีเจอร์ที่ใช้งานง่ายกว่าในการนำไปใช้กับภาษาอื่น ๆ อีกมากมายอย่างไม่สิ้นสุดรวมไปถึง:

  • การพิมพ์แบบไดนามิก
  • การสนับสนุนภายในสำหรับรายการพจนานุกรมและสตริง Unicode
  • การเพิ่มประสิทธิภาพ (JIT, การอนุมานประเภท, การคอมไพล์)
  • เครื่องมือปรับใช้แบบรวม
  • สนับสนุนห้องสมุด
  • การสนับสนุนจากชุมชนและสถานที่รวบรวม
  • ไลบรารีมาตรฐานที่หลากหลาย
  • ไวยากรณ์ที่ดี
  • อ่านวนรอบการพิมพ์ eval
  • รองรับการเขียนโปรแกรมไตร่ตรอง

3
Haskell มีการบรรทุกเกินพิกัด Class types เป็นคุณสมบัติภาษาที่ใช้เพื่อกำหนดฟังก์ชั่นที่โอเวอร์โหลด
Lii

2

คำตอบที่ดี! คำตอบของ A.Rex นั้นมีรายละเอียดและให้คำแนะนำอย่างมาก ในขณะที่เขาชี้ให้เห็น, C ++ ไม่พิจารณาผู้ประกอบการประเภทการแปลงจากผู้ใช้เมื่อรวบรวมlhs = func(); (ที่ func มันชื่อของ struct ที่) วิธีแก้ปัญหาของฉันแตกต่างกันเล็กน้อย - ไม่ดีกว่าเพียงแค่แตกต่างกัน (แม้ว่าจะขึ้นอยู่กับแนวคิดพื้นฐานเดียวกัน)

ในขณะที่ฉันอยากจะเขียน ...

template <typename T> inline T func() { abort(); return T(); }

template <> inline int func()
{ <<special code for int>> }

template <> inline double func()
{ <<special code for double>> }

.. etc, then ..

int x = func(); // ambiguous!
int x = func<int>(); // *also* ambiguous!?  you're just being difficult, g++!

ฉันสิ้นสุดด้วยโซลูชันที่ใช้ structized พารามิเตอร์ (กับ T = ประเภทผลตอบแทน):

template <typename T>
struct func
{
    operator T()
    { abort(); return T(); } 
};

// explicit specializations for supported types
// (any code that includes this header can add more!)

template <> inline
func<int>::operator int()
{ <<special code for int>> }

template <> inline
func<double>::operator double()
{ <<special code for double>> }

.. etc, then ..

int x = func<int>(); // this is OK!
double d = func<double>(); // also OK :)

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

template <typename T>
struct func<T*>
{
    operator T*()
    { <<special handling for T*>> } 
};

เป็นลบคุณไม่สามารถเขียนint x = func();ด้วยโซลูชันของฉัน int x = func<int>();คุณต้องเขียน คุณต้องบอกอย่างชัดเจนว่าประเภทการส่งคืนคืออะไรแทนที่จะขอให้คอมไพเลอร์ถอนออกโดยดูที่โอเปอเรเตอร์การแปลงประเภท ฉันจะบอกว่าวิธีแก้ปัญหา "ของฉัน" และของ A.Rex ทั้งคู่อยู่ในแนวทางที่เหมาะสมที่สุดในการจัดการกับปัญหา C ++ นี้ :)


1

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

type    
    myclass = class
    public
      function Funct1(dummy: string = EmptyStr): String; overload;
      function Funct1(dummy: Integer = -1): Integer; overload;
    end;

ใช้มันแบบนี้

procedure tester;
var yourobject : myclass;
  iValue: integer;
  sValue: string;
begin
  yourobject:= myclass.create;
  iValue:= yourobject.Funct1(); //this will call the func with integer result
  sValue:= yourobject.Funct1(); //this will call the func with string result
end;

นั่นเป็นความคิดที่แย่มาก ไม่แนะนำพารามิเตอร์ดัมมี่นั่นคือกลิ่นรหัสใหญ่ ให้เลือกชื่ออื่นหรือเลือกชนิดส่งคืนที่สามารถทำหน้าที่เหมือนหรือเป็นสหภาพที่แบ่งแยก
Abel

@Abel สิ่งที่คุณแนะนำเป็นจริงความคิดที่น่ากลัวเพราะความคิดทั้งหมดเกี่ยวกับพารามิเตอร์จำลองนี้และมีชื่อเช่นนั้นเพื่อให้ชัดเจนสำหรับนักพัฒนาว่าพารามิเตอร์นี้เป็นหุ่นและควรละเว้นเช่นกันในกรณีที่คุณ ไม่ทราบว่ามีการใช้พารามิเตอร์ดัมมี่ที่มีค่าเริ่มต้นในห้องสมุดหลายแห่ง VCL ในเดลฟีและ IDE หลายแห่งเช่นในเดลฟี่คุณสามารถดูได้ในหน่วย sysutils ใน SafeLoadLibrary ...
ZORRO_BLANCO

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

0

ดังที่แสดงแล้ว - การเรียกฟังก์ชั่นที่คลุมเครือของฟังก์ชั่นที่แตกต่างกันตามประเภทของการส่งคืนเท่านั้นทำให้เกิดความกำกวม ความคลุมเครือทำให้เกิดรหัสที่มีข้อบกพร่อง ต้องหลีกเลี่ยงรหัสที่มีข้อบกพร่อง

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

procedure(reference string){};
procedure(reference int){};
string blah;
procedure(blah)

เนื่องจากคุณไม่สามารถนำค่า "คืน" มาใช้ใหม่ได้อย่างง่ายดายในทันที คุณจะต้องทำการโทรแต่ละครั้งในบรรทัดเดียวซึ่งต่างกับdoing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());
Adowrath

0

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

public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}

หากภาษากลับมามากไปก็จะอนุญาตให้พารามิเตอร์มากไป แต่ไม่ซ้ำกัน สิ่งนี้จะช่วยแก้ปัญหาของ:

main (){
f(x)
}

เพราะมีเพียงหนึ่ง f (ตัวเลือก int) ให้เลือก


0

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

ค#

public enum FooReturnType{
        IntType,
        StringType,
        WeaType
    }

    class Wea { 
        public override string ToString()
        {
            return "Wea class";
        }
    }

    public static object Foo(FooReturnType type){
        object result = null;
        if (type == FooReturnType.IntType) 
        {
            /*Int related actions*/
            result = 1;
        }
        else if (type == FooReturnType.StringType)
        {
            /*String related actions*/
            result = "Some important text";
        }
        else if (type == FooReturnType.WeaType)
        {
            /*Wea related actions*/
            result = new Wea();
        }
        return result;
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Expecting Int from Foo: " + Foo(FooReturnType.IntType));
        Console.WriteLine("Expecting String from Foo: " + Foo(FooReturnType.StringType));
        Console.WriteLine("Expecting Wea from Foo: " + Foo(FooReturnType.WeaType));
        Console.Read();
    }

บางทีตัวอย่างนี้อาจช่วยได้เช่นกัน:

C ++

    #include <iostream>

enum class FooReturnType{ //Only C++11
    IntType,
    StringType,
    WeaType
}_FooReturnType;

class Wea{
public:
    const char* ToString(){
        return "Wea class";
    }
};

void* Foo(FooReturnType type){
    void* result = 0;
    if (type == FooReturnType::IntType) //Only C++11
    {
        /*Int related actions*/
        result = (void*)1;
    }
    else if (type == FooReturnType::StringType) //Only C++11
    {
        /*String related actions*/
        result = (void*)"Some important text";
    }
    else if (type == FooReturnType::WeaType) //Only C++11
    {
        /*Wea related actions*/
        result = (void*)new Wea();
    }
    return result;
}

int main(int argc, char* argv[])
{
    int intReturn = (int)Foo(FooReturnType::IntType);
    const char* stringReturn = (const char*)Foo(FooReturnType::StringType);
    Wea *someWea = static_cast<Wea*>(Foo(FooReturnType::WeaType));
    std::cout << "Expecting Int from Foo: " << intReturn << std::endl;
    std::cout << "Expecting String from Foo: " << stringReturn << std::endl;
    std::cout << "Expecting Wea from Foo: " << someWea->ToString() << std::endl;
    delete someWea; // Don't leak oil!
    return 0;
}

1
เป็นแฮ็กชนิดหนึ่งและอาจนำไปสู่ข้อผิดพลาดรันไทม์หากผู้ใช้ส่งผลลัพธ์ไม่ถูกต้องหรือหากผู้พัฒนาไม่ตรงกับประเภทส่งคืนกับ enum ฉันจะแนะนำให้ใช้วิธีการที่แม่แบบเช่นใน (หรือพารามิเตอร์ทั่วไปใน C #?) คำตอบนี้
sleblanc

0

สำหรับบันทึกนั้นOctaveจะให้ผลลัพธ์ที่แตกต่างกันตามองค์ประกอบการส่งคืนที่เป็นแบบสเกลาร์และอาเรย์

x = min ([1, 3, 0, 2, 0])
   ⇒  x = 0

[x, ix] = min ([1, 3, 0, 2, 0])
   ⇒  x = 0
      ix = 3 (item index)

Cf ยังสลายมูลค่าเอกพจน์


0

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

util.h

#ifndef UTIL_H
#define UTIL_H

#include <string>
#include <sstream>
#include <algorithm>

class util {
public: 
    static int      convertToInt( const std::string& str );
    static unsigned convertToUnsigned( const std::string& str );
    static float    convertToFloat( const std::string& str );
    static double   convertToDouble( const std::string& str );

private:
    util();
    util( const util& c );
    util& operator=( const util& c );

    template<typename T>
    static bool stringToValue( const std::string& str, T* pVal, unsigned numValues );

    template<typename T>
    static T getValue( const std::string& str, std::size_t& remainder );
};

#include "util.inl"

#endif UTIL_H

util.inl

template<typename T>
static bool util::stringToValue( const std::string& str, T* pValue, unsigned numValues ) {
    int numCommas = std::count(str.begin(), str.end(), ',');
    if (numCommas != numValues - 1) {
        return false;
    }

    std::size_t remainder;
    pValue[0] = getValue<T>(str, remainder);

    if (numValues == 1) {
        if (str.size() != remainder) {
            return false;
        }
    }
    else {
        std::size_t offset = remainder;
        if (str.at(offset) != ',') {
            return false;
        }

        unsigned lastIdx = numValues - 1;
        for (unsigned u = 1; u < numValues; ++u) {
            pValue[u] = getValue<T>(str.substr(++offset), remainder);
            offset += remainder;
            if ((u < lastIdx && str.at(offset) != ',') ||
                (u == lastIdx && offset != str.size()))
            {
                return false;
            }
        }
    }
    return true;    
}

util.cpp

#include "util.h"

template<>
int util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stoi( str, &remainder );
} 

template<>
unsigned util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stoul( str, &remainder );
}

template<>
float util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stof( str, &remainder );
}     

template<>   
double util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stod( str, &remainder );
}

int util::convertToInt( const std::string& str ) {
    int i = 0;
    if ( !stringToValue( str, &i, 1 ) ) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to int";
        throw strStream.str();
    }
    return i;
}

unsigned util::convertToUnsigned( const std::string& str ) {
    unsigned u = 0;
    if ( !stringToValue( str, &u, 1 ) ) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to unsigned";
        throw strStream.str();
    }
    return u;
}     

float util::convertToFloat(const std::string& str) {
    float f = 0;
    if (!stringToValue(str, &f, 1)) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to float";
        throw strStream.str();
    }
    return f;
}

double util::convertToDouble(const std::string& str) {
    float d = 0;
    if (!stringToValue(str, &d, 1)) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to double";
        throw strStream.str();
    }
    return d;
}

ตัวอย่างนี้ไม่ได้ใช้การแก้ปัญหาการโอเวอร์โหลดของฟังก์ชั่นอย่างแน่นอนตามประเภทการส่งคืนอย่างไรก็ตามคลาสอ็อบเจ็กต์ที่ไม่ใช่ c ++ นี้กำลังใช้ความเชี่ยวชาญเทมเพลตเพื่อจำลองการแก้ปัญหาการโอเวอร์โหลดของฟังก์ชั่นตามประเภทการส่งคืน

แต่ละconvertToTypeฟังก์ชั่นจะเรียกแม่แบบฟังก์ชั่นstringToValue()และถ้าคุณดูที่รายละเอียดการดำเนินการหรือขั้นตอนวิธีของแม่แบบฟังก์ชั่นนี้จะเรียกgetValue<T>( param, param )และมันจะกลับชนิดTและการจัดเก็บให้มันกลายเป็นT*ที่ถูกส่งผ่านเข้าไปในstringToValue()แม่แบบฟังก์ชั่นเป็นหนึ่งในพารามิเตอร์ .

นอกเหนือจากสิ่งนี้ C ++ ไม่มีกลไกในการรับฟังก์ชั่นการแก้ปัญหาการโอเวอร์โหลดตามประเภทการส่งคืน อาจมีโครงสร้างหรือกลไกอื่น ๆ ที่ฉันไม่ทราบว่าสามารถจำลองการแก้ไขตามประเภทการส่งคืนได้


-1

ฉันคิดว่านี่เป็น GAP ในนิยาม C ++ ที่ทันสมัย ​​... ทำไม?

int func();
double func();

// example 1. → defined
int i = func();

// example 2. → defined
double d = func();

// example 3. → NOT defined. error
void main() 
{
    func();
}

ทำไมคอมไพเลอร์ C ++ ไม่สามารถโยนข้อผิดพลาดในตัวอย่าง "3" และยอมรับรหัสในตัวอย่าง "1 + 2"


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

-2

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


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

ดูcodeproject.com/KB/cpp/returnoverload.aspxสำหรับกลยุทธ์ที่ชาญฉลาดสำหรับ "การโอเวอร์โหลดตามประเภทผลตอบแทน" โดยทั่วไปแทนที่จะกำหนดฟังก์ชั่น func () คุณกำหนด struct func ให้โอเปอเรเตอร์ () () และการแปลงเป็นประเภทที่เหมาะสมแต่ละประเภท
j_random_hacker

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