c# form外觀




在C#中使用的yield關鍵字是什麼? (11)

我如何公開只有一個IList <>的問題問題之一的答案有以下代碼片段:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item )
            yield return item;
    }
}

yield關鍵字在那裡做什麼? 我已經在幾個地方看到了它的引用,還有一個問題,但我還沒有弄清楚它的實際功能。 我習慣於從一個線程產生另一個線程的意義上考慮產量,但在這裡看起來並不相關。


yield關鍵字在這裡確實做了很多。 該函數返回一個實現IEnumerable接口的對象。 如果一個調用函數開始對這個對象進行foreach,那麼函數會被再次調用,直到它“屈服”。 這是C#2.0中引入的語法糖。 在早期版本中,您必須創建自己的IEnumerable和IEnumerator對象來執行這樣的操作。

理解這樣的代碼最簡單的方法是鍵入一個例子,設置一些斷點並查看會發生什麼。

嘗試通過這個例如:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

當你通過這個例子時,你會發現第一次調用Integers()返回1.第二次調用返回2,並且行“yield return 1”不再被執行。

這是一個真實的例子

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

yield return與統計員一起使用。 在yield語句的每次調用中,控制返回給調用者,但它確保了被調用者的狀態得以維持。 因此,當調用者枚舉下一個元素時,它將在yield語句之後立即繼續在callee方法中執行。

讓我們試著用一個例子來理解這一點。 在這個例子中,對應於每一行我都提到了執行流程的順序。

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

此外,每個枚舉都保持該狀態。 假設,我有另一個調用Fibs()方法,那麼狀態將被重置。


乍看之下,yield return是一個返回IEnumerable的.NET糖。

如果沒有收益,集合的所有項目都會立即創建:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

使用yield的相同代碼,它逐項返回:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

使用yield的好處在於,如果使用數據的函數只需要集合的第一個項目,則其餘項目將不會創建。

收益運營商可以根據需要創建項目。 這是使用它的一個很好的理由。


列表或數組實現立即加載所有項目,而yield實現提供延遲執行解決方案。

實際上,為了減少應用程序的資源消耗,通常需要根據需要執行最少量的工作。

例如,我們可能有一個應用程序處理來自數據庫的數百萬條記錄。 當我們在延遲執行的基於拉的模型中使用IEnumerable時,可以獲得以下好處:

  • 可擴展性,可靠性和可預測性可能會提高,因為記錄數量不會顯著影響應用程序的資源需求。
  • 性能和響應能力可能會提高,因為處理可以立即開始,而不是等待整個集合被首先加載。
  • 由於應用程序可以停止,啟動,中斷或失敗, 可恢復性和利用率可能會提高。 與預取所有僅實際使用一部分結果的數據相比,只有正在進行的項目會丟失。
  • 在添加恆定工作負載流的環境中可以進行連續處理

這裡比較一下如何建立一個集合,比如使用yield來比較list。

列表示例

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

控制台輸出
ContactListStore:創建聯繫人1
ContactListStore:創建聯繫人2
ContactListStore:創建聯繫人3
準備遍歷集合。

注意:整個集合都被加載到內存中,甚至沒有要求列表中的單個項目

產量示例

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

控制台輸出
準備遍歷集合。

注意:集合根本沒有執行。 這是由於IEnumerable的“延遲執行”性質。 構建一個項目只會在真正需要時才會發生。

讓我們再次調用集合,並在我們獲取集合中的第一個聯繫人時反應該行為。

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

控制台輸出
準備遍歷集合
ContactYieldStore:創建聯繫人1
鮑勃你好

太好了! 當客戶從收藏中“拉出”物品時,只有第一個聯繫人被建造。


它產生了可枚舉的序列。 它所做的實際上是創建本地IEnumerable序列並將其作為方法結果返回


它試圖引入一些Ruby Goodness :)
概念:這是一些打印出數組的每個元素的示例Ruby代碼

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

數組的每個方法實現都將控制權交給調用者('puts x'),數組中的每個元素整齊地呈現為x。 然後調用者可以做任何它需要做的事情。

然而, .Net並不是一路走來的...... C#似乎將IEnumerable與yield結合起來,這迫使你在調用者中編寫一個foreach循環,正如Mendelt的回應所見。 少一點優雅。

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}

最近Raymond Chen也在yield關鍵字上發表了一系列有趣的文章。

雖然它名義上用於輕鬆實現迭代器模式,但可以概括為狀態機。 在引用雷蒙德的時候沒有任何意義,最後一部分也與其他用途聯繫在一起(但Entin博客中的例子特別好,展示瞭如何編寫異步安全代碼)。


直觀地說,關鍵字從函數中返回一個值而不離開它,即在你的代碼示例中,它返回當前item值,然後恢復循環。 更正式地,編譯器使用它來為迭代器生成代碼。 迭代器是返回IEnumerable對象的函數。 MSDNarticles關於他們的articles


迭代。 它創建了一個“隱藏​​”的狀態機,它記住了每個額外的函數循環的位置,並從那裡開始。


這個link有一個簡單的例子

更簡單的例子就在這裡

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

請注意,收益率返回不會從方法返回。 您甚至可以在yield return後放入一個WriteLine

以上產生了一個IEnumerable 4,4,4,4,4

這裡有一個WriteLine 。 將列表添加4,打印abc,然後將4添加到列表中,然後完成方法,然後真正從方法返回(一旦方法完成,就像沒有返回的過程一樣)。 但是這會有一個值,一個IEnumerable int s列表,它會在完成時返回。

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

還要注意,當你使用yield時,你所返回的與函數的類型不一樣。 它是IEnumerable列表中元素的類型。

您使用yield與方法的返回類型為IEnumerable 。 如果方法的返回類型是intList<int>並且使用了yield ,那麼它將不會編譯。 你可以使用IEnumerable方法返回類型而不產生收益,但是如果沒有IEnumerable方法返回類型,你可能不能使用yield。

為了實現它,你必須以特殊的方式來調用它。

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

這裡有一個簡單的方法來理解這個概念:基本的想法是,如果你想要一個你可以使用“ foreach ”的集合,但是收集這些項目到集合中是很昂貴的(比如從數據庫中查詢它們) ,而且你通常不需要整個集合,那麼你創建一個函數,一次構建集合一個項目,並將其返回給消費者(然後可以儘早終止收集工作)。

這樣想:你去肉櫃檯,想買一磅切片火腿。 屠夫將一個10磅重的火腿放到後面,放在切片機上,切成片,然後將一堆切片放回給你,並測出一磅。 (舊方式)。 隨著yield ,屠夫將切片機送到櫃檯,並開始切片,並將每個切片“屈服”到秤上,直到它測量到1磅為止,然後將其包裹,然後完成。 對於屠夫來說,舊方式可能會更好(讓他按自己喜歡的方式組織自己的機器),但對於消費者來說,新方法在大多數情況下顯然更有效率。







yield