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


89

ฉันคิดว่ามันจะดีถ้าทำอะไรแบบนี้ (โดยที่แลมด้าให้ผลตอบแทน):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

อย่างไรก็ตามฉันพบว่าฉันไม่สามารถใช้ผลตอบแทนในวิธีที่ไม่ระบุตัวตนได้ ฉันสงสัยว่าทำไม เอกสารผลผลิตเพียงแค่บอกว่ามันไม่ได้รับอนุญาต

เนื่องจากไม่ได้รับอนุญาตฉันจึงสร้างรายการและเพิ่มรายการเข้าไป


ตอนนี้เราสามารถมีasynclambdas ที่ไม่ระบุตัวตนที่อนุญาตให้awaitอยู่ใน C # 5.0 ได้แล้วฉันสนใจที่จะทราบว่าเหตุใดพวกเขาจึงยังไม่ได้ใช้ตัวทำซ้ำแบบไม่ระบุตัวตนyieldภายใน ไม่มากก็น้อยมันเป็นเครื่องกำเนิดไฟฟ้าสถานะเดียวกัน
อัตราส่วนจมูก

คำตอบ:


114

Eric Lippert เพิ่งเขียนบล็อกโพสต์ชุดหนึ่งเกี่ยวกับสาเหตุที่ไม่อนุญาตให้ผลตอบแทนในบางกรณี

แก้ไข 2:

  • ตอนที่ 7 (อันนี้โพสต์ในภายหลังและตอบคำถามนี้โดยเฉพาะ)

คุณอาจจะพบคำตอบที่นั่น ...


แก้ไข 1: สิ่งนี้อธิบายไว้ในความคิดเห็นของตอนที่ 5 ในคำตอบของ Eric ต่อความคิดเห็นของ Abhijeet Patel:

ถาม:

เอริค

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

A:

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

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

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

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


2
น่าสนใจโดยเฉพาะอย่างยิ่งเนื่องจากขณะนี้มีฟังก์ชันท้องถิ่น
Mafii

4
ฉันสงสัยว่าคำตอบนี้ล้าสมัยหรือไม่เพราะจะได้รับผลตอบแทนจากฟังก์ชันท้องถิ่น
Joshua

2
@ Joshua แต่ฟังก์ชันภายในไม่เหมือนกับวิธีการที่ไม่ระบุชื่อ ... ผลตอบแทนที่ได้รับยังไม่ได้รับอนุญาตในวิธีการที่ไม่ระบุชื่อ
Thomas Levesque

21

Eric Lippert ได้เขียนบทความที่ยอดเยี่ยมเกี่ยวกับข้อ จำกัด (และการตัดสินใจออกแบบที่มีอิทธิพลต่อตัวเลือกเหล่านั้น) ในบล็อกตัวทำซ้ำ

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

ด้วยเหตุนี้จึงถูกห้ามไม่ให้มีปฏิสัมพันธ์

วิธีบล็อก iterator ทำงานภายใต้ประทุนจัดการกับดีนี่

เป็นตัวอย่างง่ายๆของความไม่ลงรอยกัน:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

คอมไพเลอร์ต้องการแปลงสิ่งนี้ไปพร้อม ๆ กันเช่น:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

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

อย่างไรก็ตามสิ่งนี้จะเป็น

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

ในตัวอย่างของคุณเป็นดังนี้:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

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

4

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

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

นอกจากนี้ยังใช้วิธีการวนซ้ำโดยyieldใช้เวทมนตร์ของคอมไพเลอร์

ฉันเดาว่าหนึ่งในสองตัวนี้ทำให้รหัสไม่สามารถระบุตัวตนได้กับเวทมนตร์ชิ้นอื่น ๆ และตัดสินใจที่จะไม่ใช้เวลาในการสร้างงานนี้กับคอมไพเลอร์ C # เวอร์ชันปัจจุบัน แน่นอนว่ามันอาจไม่ใช่ทางเลือกที่ชัดเจนและมันก็ไม่ได้ผลเพราะไม่มีใครคิดจะนำไปใช้

สำหรับคำถามที่ถูกต้อง 100% ฉันขอแนะนำให้คุณใช้ไซต์Microsoft Connectและรายงานคำถามฉันแน่ใจว่าคุณจะได้รับสิ่งที่ใช้งานได้เป็นการตอบแทน


1

ฉันจะทำสิ่งนี้:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

แน่นอนว่าคุณต้องมี System.Core.dll ที่อ้างอิงจาก. NET 3.5 สำหรับวิธี Linq และรวมถึง:

using System.Linq;

ไชโย

เจ้าเล่ห์


0

อาจเป็นเพียงข้อ จำกัด ทางไวยากรณ์ ใน Visual Basic .NET ซึ่งคล้ายกับ C # มากมันเป็นไปได้อย่างสมบูรณ์แบบในขณะที่เขียนไม่สะดวก

Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

สังเกตวงเล็บ' hereด้วย ฟังก์ชั่นแลมบ์ดาIterator Function... End Function ผลตอบแทนIEnumerable(Of Integer)แต่ไม่ได้เป็นเช่นวัตถุตัวเอง มันต้องเรียกว่าได้รับวัตถุนั้น

รหัสที่แปลงโดย [1] ทำให้เกิดข้อผิดพลาดใน C # 7.3 (CS0149):

static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}

ฉันไม่เห็นด้วยอย่างยิ่งกับเหตุผลที่ให้ไว้ในคำตอบอื่น ๆ ที่คอมไพเลอร์จะจัดการได้ยาก Iterator Function()คุณเห็นในตัวอย่าง VB.NET จะถูกสร้างขึ้นมาโดยเฉพาะสำหรับ iterators แลมบ์ดา

ใน VB มีIteratorคีย์เวิร์ด; มันไม่มีคู่ C # IMHO ไม่มีเหตุผลที่แท้จริงว่านี่ไม่ใช่คุณสมบัติของ C #

ดังนั้นหากคุณต้องการฟังก์ชั่นตัววนซ้ำแบบไม่ระบุตัวตนจริงๆปัจจุบันใช้ Visual Basic หรือ (ฉันยังไม่ได้ตรวจสอบ) F # ตามที่ระบุไว้ในความคิดเห็นของส่วนที่ # 7ในคำตอบของ @Thomas Levesque (ทำ Ctrl + F สำหรับ F #)

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