foreach vs. için LINQ için


86

Visual Studio'da kod yazdığımda ReSharper (Tanrı onu korusun!) Sık sık eski okulumu daha küçük foreach formunda döngü için değiştirmemi önerir.

Sıklıkla, bu değişikliği kabul ettiğimde, ReSharper bir adım öne geçer ve parlak bir LINQ biçiminde tekrar değiştirmemi önerir.

Öyleyse merak ediyorum: bu gelişmelerde bazı gerçek avantajlar var mı? Oldukça basit kod çalıştırmada, herhangi bir hız artışı göremiyorum (açıkçası), ancak kodun daha az okunabilir olduğunu görebiliyorum ... Yani merak ediyorum: buna değer mi?


2
Sadece bir not - SQL sözdizimini iyi biliyorsanız LINQ sözdizimi aslında oldukça okunaklıdır. LINQ (SQL benzeri lambda ifadeleri ve zincirleme yöntemleri) için öğrenmeyi kolaylaştıracak iki biçim vardır. ReSharper'ın okunaksız görünmesini sağlayan önerileri olabilir.
Shauna, 16

3
Genel bir kural olarak, bilinen bir uzunluk dizisi veya yineleme sayısının alakalı olduğu benzer durumlar ile çalışmadıkça, genellikle foreach kullanırım. LINQ-ifying'e gelince, genellikle ReSharper'ın bir foreach'den ne yaptığını göreceğim ve eğer ortaya çıkan LINQ ifadesi düzenli / önemsiz / okunabilirse onu kullanıyorum ve aksi takdirde geri döndürüyorum. Gereksinimler değiştiyse orijinal LINQ dışı mantığı yeniden yazmak için bir angarya olacaksa veya LINQ deyiminin uzaklaştığı mantıktan ayrıntılı bir şekilde hata ayıklamak gerekirse, LINQ ifadesinden uzaklaşmak istemezsem form.
Ed Hastings,

Yaygın hatalardan biri foreach, bir koleksiyondan öğeleri çıkarırken, genellikle forson elemandan başlamak için bir döngünün gerekli olduğu yerlerdedir .
Slai

Nesne Yönelimli Geliştiriciler için Øredev 2013 - Jessica Kerr - İşlevsel İlkelerden değer alabilirsiniz . Linq, 33 dakikalık işaretten hemen sonra "Declarative Style" başlığı altında sunuma gelir.
Theraot

Yanıtlar:


139

for vs. foreach

Bu iki yapının çok benzer olduğu ve her ikisinin de bunun gibi değiştirilebilir olduğu yönünde ortak bir karışıklık var:

foreach (var c in collection)
{
    DoSomething(c);
}

ve:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

Her iki anahtar kelimenin de aynı üç harfle başlaması, anlamsal olarak benzer oldukları anlamına gelmez. Bu karışıklık özellikle yeni başlayanlar için son derece hataya açıktır. Bir koleksiyonda yineleme ve unsurlarla bir şeyler yapmak foreach; forNe yaptığınızı gerçekten bilmiyorsanız, bu amaç için kullanılamaz ve kullanılmamalıdır .

Bir örnekle neyin yanlış olduğunu görelim. Sonunda, sonuçları toplamak için kullanılan bir demo uygulamasının tam kodunu bulacaksınız.

Örnekte, "Boston" ile karşılaşmadan önce veritabanına, daha doğrusu Adventure Works adlı şehirlere, adlarına göre sıralanan bazı verileri yüklüyoruz. Aşağıdaki SQL sorgusu kullanılır:

select distinct [City] from [Person].[Address] order by [City]

Veri ListCities()bir döndüren yöntem tarafından yüklenir IEnumerable<string>. İşte neye foreachbenziyor:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Her forikisinin de değiştirilebilir olduğunu varsayarak, bunu bir ile yeniden yazalım:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Her ikisi de aynı şehirleri döndürür, ancak çok büyük bir fark vardır.

  • Kullanırken foreach, ListCities()bir zamanlar denir ve 47 madde verir.
  • Kullanıldığında for, ListCities()94 kez denir ve genel olarak 28153 ürün verir.

Ne oldu?

IEnumerableolduğunu tembel . Bu, sadece sonucun gerekli olduğu anda işi yapacağı anlamına gelir. Tembel değerlendirme çok faydalı bir konsepttir, ancak sonucun gerekli olacağı anları, özellikle sonucun birden çok kez kullanıldığı durumlarda kaçırmanın kolay olduğu gerçeği gibi bazı uyarılar vardır.

Bir durumda foreach, sonuç yalnızca bir kez istenir. Yukarıdaki yanlış yazılmış kodda uygulanan bir durum halinde , sonuç 94 kezfor , yani 47x2 olarak talep edilir :

  • Her zaman cities.Count()denir (47 kez),

  • Her zaman cities.ElementAt(i)denir (47 kez).

Bir veritabanını biri yerine 94 kez sorgulamak korkunç, ancak olabilecek en kötü şey değil. Örneğin, selectsorgudan önce tabloya bir satır ekleyen bir sorgudan önce gelirse ne olacağını hayal edin . Doğru, umarım daha önce çökmediği sürece forveritabanını 2,147,483,647 kez arayacaktık .

Tabii ki kodum önyargılı. Ben tembelliğini kasıtlı olarak kullandım ve IEnumerabletekrar tekrar arayacak şekilde yazdım ListCities(). Bir aceminin bunu asla yapamayacağına dikkat çekilebilir, çünkü:

  • Özelliği IEnumerable<T>değil Count, yalnızca yöntem var Count(). Bir yöntem çağırmak korkutucu ve sonuçta önbelleklenmemesi ve bir for (; ...; )blokta uygun olmaması beklenebilir .

  • Dizin oluşturma için kullanılamıyor IEnumerable<T>ve ElementAtLINQ uzantı yöntemini bulmak açık değil .

Muhtemelen çoğu yeni başlayanlar ListCities(), a List<T>.

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Yine de, bu kod foreachalternatiften çok farklı . Yine, aynı sonuçları verir ve bu kez ListCities()yöntem sadece bir kez çağrılır, ancak 575 öğe verirken foreach, yalnızca 47 öğe verir.

Aradaki fark, tüm verilerin veritabanından yüklenmesine ToList()neden olması gerçeğinden geliyor . İken "Boston" önce sadece şehirler talep, yeni alınan ve bellekte saklanmasını tüm şehirleri gerektirir. 575 kısa dizeyle büyük olasılıkla pek farketmez, fakat milyarlarca kayıt içeren bir tablodan sadece birkaç satır alıyor olsaydık ne olurdu?foreachfor

Peki foreachgerçekte nedir?

foreachbir süre döngüsüne daha yakın. Daha önce kullandığım kod:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

sadece tarafından değiştirilebilir:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

Her ikisi de aynı IL'yi üretir. Her ikisi de aynı sonuca sahiptir. Her ikisi de aynı yan etkiye sahiptir. Tabii ki, bu whilebenzer bir sonsuzlukta yeniden yazılabilir for, ancak daha uzun ve hataya açık olacaktır. Daha okunaklı bulduğunuz birini seçmekte özgürsünüz.

Kendin denemek ister misin? İşte tam kod:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

Ve sonuçlar:

--- için ---
Abingdon Albany İskenderiye Alhambra [...] Bonn Bordeaux Boston

Veriler 94 kez çağrıldı ve 28153 madde verdi.

--- liste için ile ---
Abingdon Albany İskenderiye Alhambra [...] Bonn Bordeaux Boston

Verilere 1 kez adı verildi ve 575 madde elde edildi.

--- iken ---
Abingdon Albany İskenderiye Elhamra [...] Bonn Bordeaux Boston

Verilere 1 kez adı verildi ve 47 madde verdi.

--- foreach ---
Abingdon Albany İskenderiye Elhamra [...] Bonn Bordeaux Boston

Verilere 1 kez adı verildi ve 47 madde verdi.

LINQ - geleneksel yolla

LINQ gelince, fonksiyonel programlama (FP) öğrenmek isteyebilirsiniz - C # FP şeyler değil, Haskell gibi gerçek FP dili. İşlevsel dillerin kodu ifade etmek ve sunmak için belirli bir yolu vardır. Bazı durumlarda, işlevsel olmayan paradigmalardan üstündür.

FP, listeleri manipüle etme konusunda çok daha üstün olduğu bilinmektedir ( listeyle ilgisi olmayan genel bir terim olarak listelenir List<T>). Bu gerçeği göz önüne alındığında, C # kodunu listeler söz konusu olduğunda daha işlevsel bir şekilde ifade etme yeteneği oldukça iyi bir şeydir.

Eğer ikna olmadım söylüyorsanïz işlevsel ve işlevsel olmayan yollarla hem de yazılı kodun okunabilirliği karşılaştırmak önceki cevabı konuda.


1
ListCities () örneği hakkında soru. Neden sadece bir kere yayınlansın? Geçmişte verim getirileri konusunda herhangi bir sorun yaşamadım
Dante

1
IEnumerable'dan yalnızca bir sonuç alacağınızı söylemiyor - SQL sorgusunun (yöntemin pahalı kısmı olan) yalnızca bir kez çalıştırılacağını söylüyor - bu iyi bir şey. Daha sonra sorgudan elde edilen tüm sonuçları okur ve verir.
HappyCat

9
@Giorgio: Bu soru anlaşılabilir olsa da, bir dilin anlambilimine sahip olmak, bir aceminin kafa karıştırıcı bulabileceği şeyleri ön plana çıkarır, bizi çok etkili bir dilde bırakmaz.
Steven Evers

4
LINQ sadece anlamsal şeker değildir. Gecikmeli işlem sağlar. Ve IQueryables durumunda (örn. Entity Framework), sorgunun yinelenene kadar iletilmesine ve oluşturulmasına izin verir (yani, iade edilen bir IQueryable'e bir nerede bir yan tümce eklendiğinde, italyan bir maddenin yan tümce içine konması için sunucuya aktarılmasıyla sonuçlanacaktır.) filtrelemenin sunucuya boşaltılması).
Michael Brown,

8
Bu cevabı sevdiğim kadarıyla, örneklerin bir şekilde kesinti olduğunu düşünüyorum. Sondaki özet, bunun aslında, eşitsizlik kasten kırılan bir kodun sonucu olduğunda foreach, daha verimli olduğunu göstermektedir for. Cevabın tamlığı kendisini kurtarır, ancak sıradan bir gözlemcinin yanlış sonuçlara nasıl varacağını görmek kolaydır.
Robert Harvey,

19

Zaten foreach ve foreach arasındaki farklar hakkında bazı büyük sergiler varken. LINQ'un rolünün bazı yanlış beyanları var.

LINQ sözdizimi sadece C # 'ya işlevsel bir programlama yaklaşımı veren sözdizimsel şeker değildir. LINQ, C # 'ya yönelik tüm faydaları içeren İşlevsel yapıları sağlar. IList yerine IEnumerable döndürme ile birlikte, LINQ yinelemenin ertelenmesini sağlar. İnsanların şu anda yaptıkları şey, bir IList'i böyle işlevlerinden inşa etmek ve döndürmektir.

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

Bunun yerine, ertelenmiş bir numaralandırma oluşturmak için verim dönüşü sözdizimini kullanın.

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

Şimdi, listeleme ya da yineleme işlemine kadar numaralandırma gerçekleşmez. Ve sadece gerektiği gibi gerçekleşir (işte yığın taşması problemi olmayan Fibbonaci sayımı)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

Fibonacci işlevi üzerinde bir foreach gerçekleştirme, 46 dizisini döndürür. 30'unu istiyorsanız, hesaplanacak olanın hepsi budur.

var thirtiethFib=Fibonacci().Skip(29).Take(1);

Çok eğlendiğimiz bir yerde, lambda ifadeleri için dil desteği (IQueryable ve IQueryProvider yapılarıyla birleştirildiğinde, bu, çeşitli veri kümelerine karşı sorguların işlevsel bir şekilde oluşturulmasını sağlar; ifadeler ve kaynağın yerel yapılarını kullanarak bir sorgu oluşturma ve yürütme). Nitty cesur ayrıntılara girmeyeceğim, ancak burada bir SQL Sorgu Sağlayıcısı oluşturmayı gösteren bir dizi blog yazısı var

Özet olarak, işlevinizin tüketicileri basit bir yineleme gerçekleştirecekleri zaman IEist’in IEumumerable değerini döndürmeyi tercih etmelisiniz. LINQ'un yeteneklerini, ihtiyaç duyulana kadar karmaşık sorguların yürütülmesini ertelemek için kullanın.


13

ancak kodun daha az okunabilir olduğunu görebiliyorum

Okunabilirlik, sahibinin gözündedir. Bazı insanlar söyleyebilir

var common = list1.Intersect(list2);

mükemmel okunabilir; diğerleri bunun opak olduğunu ve tercih edeceğini söyleyebilirler.

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

ne yapıldığını netleştirmek gibi. Size daha okunaklı bulduğunuzu söyleyemeyiz. Ama burada inşa ettiğim örnekte kendi önyargımın bir kısmını tespit edebiliyor olabilirsiniz ...


28
Dürüst olmak gerekirse, linq'in amacı nesneyi nesnel olarak daha okunaklı kılarken, döngüler mekanizmayı nesnel olarak daha okunaklı kılar.
jk.

16
For-for versiyonunun, kesişen versiyondan daha okunabilir olduğunu söyleyen birinden olabildiğince hızlı koşardım.
Konamiman

3
@Konamiman - Bu, bir kişinin “okunabilirliği” düşündüğü zaman ne aradığına bağlı olacaktır . jk.'in yorumu bunu mükemmel bir şekilde gösteriyor. Döngü, son sonucunu nasıl aldığını kolayca görebilmeniz anlamında daha kolay okunurken, LINQ sonuç sonucunun ne olması gerektiği konusunda daha okunaklıdır .
Shauna

2
Bu yüzden döngü uygulamaya giriyor ve sonra Intersect'i her yerde kullanıyorsunuz.
R. Martinho Fernandes

8
@Shauna: For-loop versiyonunu, başka şeyler yapan bir metodun içinde düşünün; bu bir karmaşa. Yani, doğal olarak, kendi yöntemine ayırıyorsunuz. Okunabilirlik açısından, bu, <n> IEnumerable <T> ile aynıdır. Kesin, ancak şimdi çerçeve işlevselliğini çoğalttınız ve korumak için daha fazla kod girdiniz. Tek mazeret davranışsal nedenlerden ötürü özel bir uygulamaya ihtiyacınız varsa, ancak burada yalnızca okunabilirlikten bahsediyoruz.
Misko

7

LINQ ve foreachgerçekten arasındaki fark iki farklı programlama stiline kadar azalıyor: zorunlu ve bildirimsel.

  • ZORUNLU: Bu tarzda bilgisayara "bunu yap ... şimdi yap şunu ... şimdi yap bunu" deyin. Her seferinde bir adım bir program beslersiniz.

  • Bildirim: Bu tarzda, bilgisayara sonucun ne olmasını istediğinizi söylersiniz ve oraya nasıl gidileceğini bulmasını sağlarsınız.

Bu iki stilin klasik bir örneği, derleme kodunu (veya C) SQL ile karşılaştırmaktır. Montajda birer birer (tam anlamıyla) talimat verirsiniz. SQL'de verinin nasıl birleştirileceğini ve bu verilerden istediğiniz sonucu ifade edersiniz.

Bildirici programlamanın güzel bir yan etkisi, biraz daha yüksek olma eğiliminde olmasıdır. Bu, kodunuzu değiştirmek zorunda kalmadan platformun altında gelişmesine olanak sağlar. Örneğin:

var foo = bar.Distinct();

Burada ne oluyor? Farklı bir çekirdek kullanıyor mu? İki? Elli? Bilmiyoruz ve umursamıyoruz. .NET devs, herhangi bir zamanda, kod güncellemesinden sonra kodumuzu sihirli bir şekilde hızlandırabildiğinden, aynı amaca ulaşmaya devam ettiği sürece yeniden yazabilir.

Bu, işlevsel programlamanın gücüdür. Ve bu kodu Clojure, F # ve C # gibi dillerde (işlevsel bir programlama zihniyetiyle yazılmış) bulmanızın nedeni, zorunlu emsallerinden 3x-10x daha küçüktür.

Sonunda bildirim stilini seviyorum çünkü C # 'da çoğu zaman bu, verileri değiştirmeyen kod yazmamı sağlıyor. Yukarıdaki örnekte, Distinct()çubuğu değiştirmez, verinin yeni bir kopyasını döndürür. Bu, her ne olursa olsun çubuk ve nereden geldiği aniden değişmediği anlamına gelir.

Diğer afişlerin dediği gibi, işlevsel programlamayı öğrenin. Hayatını değiştirecek. Ve eğer yapabilirseniz, gerçek bir işlevsel programlama dilinde yapın. Clojure'u tercih ediyorum, ancak F # ve Haskell de mükemmel seçimler.


2
LINQ işlemesi, gerçekte yinelene kadar ertelenir. var foo = bar.Distinct()esasen bir IEnumerator<T>siz .ToList()veya aranana kadar .ToArray(). Bu önemli bir ayrımdır, çünkü bunun farkında değilseniz hataları anlamakta zorlanabileceğini unutmayın.
Berin Loritsch

-5

Ekipteki diğer geliştiriciler LINQ'i okuyabilir mi?

Kullanmazsanız, kullanmayın yoksa iki şeyden biri olur:

  1. Kodunuz sürdürülemez olacak
  2. Tüm kodunuzu ve ona bağlı her şeyi korumanıza sıkışıp kalacaksınız

Her döngü için bir liste içinde yineleme yapmak için mükemmeldir, ancak ne yapacağınız bu değilse, bir tane kullanmayın.


11
hmm, tek bir proje için bunun cevabı olabileceğini takdir ediyorum, ancak orta ve uzun vadede personelinizi eğitmelisiniz, aksi halde iyi bir fikir gibi görünmeyen kod anlama seviyesine kadar bir yarışınız olur.
jk.

21
Aslında, olabilecek üçüncü bir şey var: diğer geliştiriciler küçük bir çaba harcayabilir ve aslında yeni ve faydalı bir şeyler öğrenebilirler. Duyulmamış değil.
Eric King

6
@InvertedLlama, geliştiricilerin yeni dil kavramlarını anlamak için örgün eğitime ihtiyaç duyduğu bir şirkette olsaydım, o zaman yeni bir şirket bulmayı düşünüyorum.
Wyatt Barnett

13
Belki de kütüphanelerdeki bu tutumdan kurtulabilirsiniz, fakat temel dil özellikleri söz konusu olduğunda, bu onu kesmiyor. Çerçeveleri seçip seçebilirsiniz. Ancak iyi bir .NET programcısı, dilin ve çekirdek platformun her bir özelliğini anlamalıdır (Sistem. *). Ve Linq kullanmadan EF'i bile düzgün kullanamayacağınızı düşününce, şunu söylemeliyim ki ... bugün ve yaşta, eğer bir .NET programcısıysanız ve Linq'i bilmiyorsanız, beceriksizsiniz.
Timothy Baldridge 16

7
Bu zaten yeterince aşağıya sahip, bu yüzden buna ekleyemem, ama cahil / yetersiz iş arkadaşlarını destekleyen bir argüman hiçbir zaman geçerli değildir.
Steven Evers,
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.