for
เมื่อเทียบกับ foreach
มีความสับสนร่วมกันว่าสิ่งก่อสร้างทั้งสองนั้นมีความคล้ายคลึงกันมากและทั้งสองสิ่งนี้สามารถแลกเปลี่ยนกันได้เช่นนี้
foreach (var c in collection)
{
DoSomething(c);
}
และ:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
ความจริงที่ว่าคำหลักทั้งสองเริ่มต้นด้วยตัวอักษรสามตัวที่เหมือนกันไม่ได้หมายความว่ามีความหมายเหมือนกัน ความสับสนนี้เป็นข้อผิดพลาดอย่างมากโดยเฉพาะอย่างยิ่งสำหรับผู้เริ่มต้น ทำซ้ำผ่านคอลเลกชันและการทำบางสิ่งบางอย่างที่มีองค์ประกอบที่จะทำกับforeach
; for
ไม่จำเป็นและไม่ควรใช้เพื่อจุดประสงค์นี้เว้นแต่คุณจะรู้ว่าคุณกำลังทำอะไรอยู่
ลองดูว่ามีอะไรผิดปกติกับมันด้วยตัวอย่าง ในตอนท้ายคุณจะพบรหัสเต็มของแอปพลิเคชันตัวอย่างที่ใช้เพื่อรวบรวมผลลัพธ์
ในตัวอย่างเรากำลังโหลดข้อมูลบางส่วนจากฐานข้อมูลอย่างแม่นยำยิ่งขึ้นเมืองต่าง ๆ จาก Adventure Works เรียงตามชื่อก่อนที่จะพบ "Boston" ใช้แบบสอบถาม SQL ต่อไปนี้:
select distinct [City] from [Person].[Address] order by [City]
ข้อมูลจะถูกโหลดโดยวิธีการซึ่งส่งกลับListCities()
IEnumerable<string>
นี่คือสิ่งที่foreach
ดูเหมือนว่า:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
ลองเขียนใหม่ด้วย a for
โดยสมมติว่าทั้งคู่สามารถใช้แทนกันได้:
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
ทั้งสองกลับมาที่เมืองเดียวกัน แต่มีความแตกต่างกันมาก
- เมื่อใช้
foreach
, ListCities()
เรียกว่าหนึ่งครั้งและอัตราผลตอบแทน 47 รายการ
- เมื่อใช้
for
, ListCities()
เรียกว่า 94 ครั้งและผลตอบแทนถัวเฉลี่ย 28,153 รายการโดยรวม
เกิดอะไรขึ้น?
IEnumerable
คือขี้เกียจ มันหมายความว่ามันจะทำงานเฉพาะในช่วงเวลาที่ต้องการผลลัพธ์ การประเมิน Lazy เป็นแนวคิดที่มีประโยชน์มาก แต่มีข้อควรระวังบางประการรวมถึงความจริงที่ว่าพลาดช่วงเวลาที่ต้องการผลลัพธ์โดยง่ายโดยเฉพาะอย่างยิ่งในกรณีที่ใช้ผลลัพธ์หลายครั้ง
ในกรณีของ a foreach
ผลลัพธ์จะถูกขอเพียงครั้งเดียว ในกรณีที่มีfor
การนำมาใช้ในโค้ดที่เขียนไม่ถูกต้องด้านบนผลลัพธ์จะถูกร้องขอ 94 ครั้งคือ 47 × 2:
การสืบค้นฐานข้อมูล 94 ครั้งแทนที่จะเป็นหนึ่งครั้งนั้นแย่มาก แต่ไม่ใช่สิ่งเลวร้ายที่อาจเกิดขึ้น ลองจินตนาการถึงตัวอย่างเช่นจะเกิดอะไรขึ้นหากselect
แบบสอบถามจะนำหน้าด้วยคิวรีที่แทรกแถวในตารางด้วย ขวาเราจะมีfor
ที่จะเรียกฐานข้อมูล2,147,483,647ครั้งเว้นแต่จะหวังว่าจะเกิดปัญหามาก่อน
แน่นอนรหัสของฉันลำเอียง ฉันจงใจใช้ความเกียจคร้านของและเขียนมันในทางที่จะเรียกซ้ำIEnumerable
ListCities()
เราสามารถทราบได้ว่าผู้เริ่มต้นจะไม่ทำเช่นนั้นเพราะ:
IEnumerable<T>
ไม่ได้มีทรัพย์สินแต่เพียงวิธีการที่Count
Count()
การเรียกเมธอดน่ากลัวและใคร ๆ ก็สามารถคาดหวังได้ว่าผลลัพธ์จะไม่ถูกแคชและไม่เหมาะในfor (; ...; )
บล็อก
ดัชนีไม่พร้อมใช้งานสำหรับIEnumerable<T>
และก็ไม่ชัดเจนที่จะหาElementAt
วิธีขยาย LINQ
อาจจะเริ่มต้นส่วนใหญ่ก็จะแปลงผลมาจากการที่จะสิ่งที่พวกเขามีความคุ้นเคยกับเช่นListCities()
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;
}
}
ถึงกระนั้นรหัสนี้แตกต่างจากforeach
ทางเลือกมาก อีกครั้งมันให้ผลลัพธ์ที่เหมือนกันและในเวลานี้ListCities()
วิธีการนั้นถูกเรียกเพียงครั้งเดียวแต่ให้ผลลัพธ์ 575 รายการในขณะที่ด้วยforeach
จะให้ผลลัพธ์เพียง 47 รายการ
ความแตกต่างมาจากข้อเท็จจริงที่ToList()
ทำให้ข้อมูลทั้งหมดถูกโหลดจากฐานข้อมูล ในขณะที่foreach
ขอเฉพาะเมืองก่อน "บอสตัน" เมืองใหม่for
จะต้องเรียกคืนและจัดเก็บเมืองทั้งหมดในหน่วยความจำ ด้วยสตริงสั้น ๆ 575 มันอาจไม่ได้สร้างความแตกต่างมากนัก แต่ถ้าเราดึงข้อมูลเพียงไม่กี่แถวจากตารางที่มีเรคคอร์ดหลายพันล้านรายการ
ดังนั้นคืออะไรforeach
จริง ๆ
foreach
อยู่ใกล้กับลูปสักครู่ รหัสที่ฉันใช้ก่อนหน้านี้:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
สามารถถูกแทนที่ด้วย:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
ทั้งสองผลิต IL เดียวกัน ทั้งคู่มีผลลัพธ์เหมือนกัน ทั้งสองมีผลข้างเคียงที่เหมือนกัน แน่นอนว่าสิ่งนี้while
สามารถเขียนใหม่ได้ในรูปแบบที่ไม่มีที่สิ้นสุดที่คล้ายกันfor
แต่อาจจะนานกว่าและมีข้อผิดพลาดได้ง่าย คุณมีอิสระที่จะเลือกสิ่งที่คุณอ่านได้มากขึ้น
ต้องการทดสอบด้วยตัวเอง? นี่คือรหัสเต็ม:
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();
}
}
}
}
}
}
และผลลัพธ์:
--- สำหรับ ---
Abingdon Albany Alexandria Alhambra [... ] Bonn Bordeaux Boston
ข้อมูลนี้ถูกเรียกว่า 94 ครั้งและให้ผล 28153 รายการ
--- สำหรับรายชื่อ ---
Abingdon Albany Alexandria Alhambra [... ] Bonn Bordeaux Boston
ข้อมูลถูกเรียก 1 ครั้งและให้ผล 575 รายการ
--- ในขณะที่ ---
Abingdon Albany Alexandria Alhambra [... ] Bonn Bordeaux Boston
ข้อมูลถูกเรียก 1 ครั้งและให้ผล 47 รายการ
--- foreach ---
Abingdon Albany Alexandria Alhambra [... ] บอนน์บอร์โดซ์บอสตัน
ข้อมูลถูกเรียก 1 ครั้งและให้ผล 47 รายการ
LINQ กับวิธีดั้งเดิม
สำหรับ LINQ คุณอาจต้องการเรียนรู้ฟังก์ชั่นการเขียนโปรแกรม (FP) ไม่ใช่สิ่ง C # FP แต่ภาษา FP จริงเช่น Haskell ภาษาหน้าที่มีวิธีการเฉพาะในการแสดงและนำเสนอรหัส ในบางสถานการณ์มันจะดีกว่ากระบวนทัศน์ที่ไม่ทำงาน
FP เป็นที่รู้จักกันดีกว่ามากเมื่อพูดถึงการจัดการรายการต่างๆ ( รายการเป็นคำทั่วไปที่ไม่เกี่ยวข้องกับList<T>
) จากข้อเท็จจริงนี้ความสามารถในการแสดงรหัส C # ในลักษณะที่ใช้งานได้มากกว่าเมื่อพูดถึงรายการต่าง ๆ เป็นสิ่งที่ดี
หากคุณไม่มั่นใจให้เปรียบเทียบความสามารถในการอ่านของโค้ดที่เขียนทั้งในรูปแบบการใช้งานและไม่ใช้งานในคำตอบก่อนหน้าของฉันในเรื่อง