นอกเหนือจาก .NET: การค้นหาสิ่งที่เทียบเท่า LINQ ใน Python, Java และ C++

นักพัฒนาที่ทำงานในระบบนิเวศ Microsoft .NET มักจะพึ่งพา Language Integrated Query (LINQ) เป็นอย่างมาก คุณสมบัติอันทรงพลังนี้ช่วยให้สามารถสอบถาม (query) แหล่งข้อมูลต่างๆ ไม่ว่าจะเป็นคอลเลกชัน ฐานข้อมูล หรือ XML โดยใช้ไวยากรณ์ที่ให้ความรู้สึกเป็นธรรมชาติในภาษา C# หรือ VB.NET มันเปลี่ยนการจัดการข้อมูลจากการวนลูปแบบสั่งการ (imperative loops) ไปเป็นคำสั่งเชิงประกาศ (declarative statements) ซึ่งช่วยปรับปรุงความสามารถในการอ่านโค้ดและความกระชับ แต่จะเกิดอะไรขึ้นเมื่อนักพัฒนาต้องก้าวออกจากขอบเขตของ .NET? โปรแกรมเมอร์จะบรรลุความสามารถในการสอบถามข้อมูลที่แสดงออกได้ชัดเจนคล้ายกันในภาษาอย่าง Python, Java หรือ C++ ได้อย่างไร? โชคดีที่แนวคิดหลักที่อยู่เบื้องหลัง LINQ ไม่ได้จำกัดอยู่เฉพาะใน .NET และมีสิ่งเทียบเท่าและทางเลือกที่มีประสิทธิภาพอยู่มากมายในวงการการเขียนโปรแกรม

ทบทวน LINQ แบบรวดเร็ว

ก่อนที่จะสำรวจทางเลือกอื่น ๆ เรามาทบทวนสั้นๆ ว่า LINQ นำเสนออะไรบ้าง LINQ ซึ่งเปิดตัวพร้อมกับ .NET Framework 3.5 เป็นวิธีการรวมศูนย์สำหรับการสอบถามข้อมูลโดยไม่คำนึงถึงแหล่งที่มา มันผสานนิพจน์คิวรี (query expressions) เข้ากับภาษาโดยตรง ซึ่งคล้ายกับคำสั่ง SQL คุณสมบัติหลัก ได้แก่:

  • ไวยากรณ์เชิงประกาศ (Declarative Syntax): คุณระบุ ว่า ต้องการข้อมูลอะไร ไม่ใช่ วิธี การดึงข้อมูลทีละขั้นตอน
  • ความปลอดภัยด้านชนิดข้อมูล (Type Safety): คิวรีส่วนใหญ่จะถูกตรวจสอบ ณ เวลาคอมไพล์ ซึ่งช่วยลดข้อผิดพลาดขณะรันไทม์
  • ตัวดำเนินการคิวรีมาตรฐาน (Standard Query Operators): ชุดเมธอดที่หลากหลาย เช่น Where (การคัดกรอง), Select (การฉายภาพ/การแปลงข้อมูล), OrderBy (การเรียงลำดับ), GroupBy (การจัดกลุ่ม), Join, Aggregate และอื่นๆ
  • การประมวลผลแบบรอเรียกใช้ (Deferred Execution): โดยทั่วไป คิวรีจะไม่ถูกประมวลผลจนกว่าผลลัพธ์จะถูกแจกแจงค่าออกมาจริงๆ ซึ่งช่วยให้สามารถปรับให้เหมาะสมและประกอบรวมคิวรีได้
  • ความสามารถในการขยาย (Extensibility): Providers ช่วยให้ LINQ ทำงานกับแหล่งข้อมูลที่แตกต่างกันได้ (Objects, SQL, XML, Entities)

ความสะดวกในการเขียน var results = collection.Where(x => x.IsValid).Select(x => x.Name); นั้นไม่อาจปฏิเสธได้ ลองมาดูกันว่าภาษาอื่น ๆ จัดการกับงานที่คล้ายกันอย่างไร

แนวทางของ Python ที่คล้ายกับ LINQ: Comprehensions และไลบรารี

Python มีกลไกหลายอย่าง ตั้งแต่คุณสมบัติในตัวที่เป็นธรรมชาติของภาษาไปจนถึงไลบรารีเฉพาะทาง ที่ให้ความสามารถคล้าย LINQ แนวทางเหล่านี้ช่วยให้นักพัฒนาสามารถทำการคัดกรอง การแปลงข้อมูล และการรวมข้อมูลได้อย่างกระชับและอ่านง่าย

List Comprehensions และ Generator Expressions

วิธีที่เป็นธรรมชาติที่สุดของ Python (Pythonic) ในการทำการคัดกรอง (Where) และการแปลงข้อมูล (Select) แบบง่ายๆ มักจะใช้ list comprehensions หรือ generator expressions

  • List Comprehension: สร้าง list ใหม่ในหน่วยความจำทันที เหมาะเมื่อคุณต้องการชุดผลลัพธ์ทั้งหมดในทันที
    numbers = [1, 2, 3, 4, 5, 6]
    # LINQ: numbers.Where(n => n % 2 == 0).Select(n => n * n)
    squared_evens = [n * n for n in numbers if n % 2 == 0]
    # ผลลัพธ์: [4, 16, 36]
    
  • Generator Expression: สร้าง iterator ที่ให้ค่าตามความต้องการ ช่วยประหยัดหน่วยความจำและเปิดใช้งานการประมวลผลแบบรอเรียกใช้คล้ายกับ LINQ ใช้วงเล็บกลมแทนวงเล็บเหลี่ยม
    numbers = [1, 2, 3, 4, 5, 6]
    # LINQ: numbers.Where(n => n % 2 == 0).Select(n => n * n)
    squared_evens_gen = (n * n for n in numbers if n % 2 == 0)
    # หากต้องการผลลัพธ์ คุณต้องวนซ้ำผ่านมัน (เช่น list(squared_evens_gen))
    # ค่าต่างๆ จะถูกคำนวณเมื่อจำเป็นในระหว่างการวนซ้ำเท่านั้น
    

ฟังก์ชันในตัวและ itertools

ตัวดำเนินการ LINQ มาตรฐานหลายตัวมีฟังก์ชันที่เทียบเท่าโดยตรงหรือใกล้เคียงในฟังก์ชันในตัวของ Python หรือโมดูล itertools ที่ทรงพลัง:

  • any(), all(): เทียบเท่าโดยตรงกับ Any และ All ของ LINQ สำหรับการตรวจสอบเงื่อนไขกับองค์ประกอบต่างๆ
    fruit = ['apple', 'orange', 'banana']
    # LINQ: fruit.Any(f => f.Contains("a"))
    any_a = any("a" in f for f in fruit) # True
    # LINQ: fruit.All(f => f.Length > 3)
    all_long = all(len(f) > 3 for f in fruit) # True
    
  • min(), max(), sum(): คล้ายกับเมธอดรวมข้อมูล (aggregation) ของ LINQ สามารถทำงานกับ iterables โดยตรงหรือรับ generator expression ได้
    numbers = [1, 5, 2, 8, 3]
    # LINQ: numbers.Max()
    maximum = max(numbers) # 8
    # LINQ: numbers.Where(n => n % 2 != 0).Sum()
    odd_sum = sum(n for n in numbers if n % 2 != 0) # 1 + 5 + 3 = 9
    
  • filter(), map(): ฟังก์ชันที่ทำงานคล้ายกับ Where และ Select ในเชิงฟังก์ชันนัล ใน Python 3 ฟังก์ชันเหล่านี้จะคืนค่าเป็น iterators ซึ่งส่งเสริมการประเมินผลแบบ lazy
    numbers = [1, 2, 3, 4]
    # LINQ: numbers.Where(n => n > 2)
    filtered_iter = filter(lambda n: n > 2, numbers) # ให้ค่า 3, 4 เมื่อวนซ้ำ
    # LINQ: numbers.Select(n => n * 2)
    mapped_iter = map(lambda n: n * 2, numbers) # ให้ค่า 2, 4, 6, 8 เมื่อวนซ้ำ
    
  • sorted(): เทียบเท่ากับ OrderBy รับฟังก์ชัน key (optional) สำหรับระบุเกณฑ์การเรียงลำดับและคืนค่าเป็น list ใหม่ ที่เรียงลำดับแล้ว
    fruit = ['pear', 'apple', 'banana']
    # LINQ: fruit.OrderBy(f => f.Length)
    sorted_fruit = sorted(fruit, key=len) # ['pear', 'apple', 'banana']
    
  • itertools.islice(iterable, stop) หรือ itertools.islice(iterable, start, stop[, step]): ทำหน้าที่เหมือน Take และ Skip คืนค่าเป็น iterator
    from itertools import islice
    numbers = [0, 1, 2, 3, 4, 5]
    # LINQ: numbers.Take(3)
    first_three = list(islice(numbers, 3)) # [0, 1, 2]
    # LINQ: numbers.Skip(2)
    skip_two = list(islice(numbers, 2, None)) # [2, 3, 4, 5]
    # LINQ: numbers.Skip(1).Take(2)
    skip_one_take_two = list(islice(numbers, 1, 3)) # [1, 2]
    
  • itertools.takewhile(), itertools.dropwhile(): เทียบเท่ากับ TakeWhile และ SkipWhile ทำงานตามเงื่อนไข (predicate)
    from itertools import takewhile, dropwhile
    numbers = [2, 4, 6, 7, 8, 10]
    # LINQ: numbers.TakeWhile(n => n % 2 == 0)
    take_evens = list(takewhile(lambda n: n % 2 == 0, numbers)) # [2, 4, 6]
    # LINQ: numbers.SkipWhile(n => n % 2 == 0)
    skip_evens = list(dropwhile(lambda n: n % 2 == 0, numbers)) # [7, 8, 10]
    
  • itertools.groupby(): คล้ายกับ GroupBy แต่ต้องการให้ iterable ที่ป้อนเข้ามาต้องถูกเรียงลำดับตามคีย์การจัดกลุ่ม ก่อน เพื่อให้องค์ประกอบถูกจัดกลุ่มได้อย่างถูกต้อง คืนค่าเป็น iterator ที่ให้ค่าคู่ (key, group_iterator)
    from itertools import groupby
    
    fruit = ['apple', 'apricot', 'banana', 'blueberry', 'cherry']
    # ต้องเรียงลำดับตามคีย์ก่อนเพื่อให้ groupby ทำงานตามที่คาดหวังในกรณีส่วนใหญ่
    keyfunc = lambda f: f[0] # จัดกลุ่มตามตัวอักษรตัวแรก
    sorted_fruit = sorted(fruit, key=keyfunc)
    # LINQ: fruit.GroupBy(f => f[0])
    grouped_fruit = groupby(sorted_fruit, key=keyfunc)
    for key, group_iter in grouped_fruit:
        print(f"{key}: {list(group_iter)}")
    # ผลลัพธ์:
    # a: ['apple', 'apricot']
    # b: ['banana', 'blueberry']
    # c: ['cherry']
    
  • set(): สามารถใช้สำหรับ Distinct ได้ แต่ไม่รักษาลำดับเดิม
    numbers = [1, 2, 2, 3, 1, 4, 3]
    # LINQ: numbers.Distinct()
    distinct_numbers_set = set(numbers) # ไม่รับประกันลำดับ เช่น {1, 2, 3, 4}
    distinct_numbers_list = list(distinct_numbers_set) # เช่น [1, 2, 3, 4]
    
    # สำหรับการหาค่าไม่ซ้ำที่รักษาลำดับ:
    seen = set()
    distinct_ordered = [x for x in numbers if not (x in seen or seen.add(x))] # [1, 2, 3, 4]
    

ไลบรารี py-linq

สำหรับนักพัฒนาที่ต้องการไวยากรณ์การเชื่อมต่อเมธอด (method chaining) และรูปแบบการตั้งชื่อเฉพาะของ LINQ ใน .NET ไลบรารี py-linq นำเสนอการพอร์ตโดยตรง หลังจากติดตั้ง (pip install py-linq) คุณจะต้องห่อหุ้มคอลเลกชันของคุณด้วยออบเจกต์ Enumerable

from py_linq import Enumerable

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __repr__(self):
        return f'{self.name} ({self.age})'

people = [Person('Alice', 30), Person('Bob', 20), Person('Charlie', 25)]

e = Enumerable(people)

# LINQ: people.Where(p => p.age > 21).OrderBy(p => p.name).Select(p => p.name)
results = e.where(lambda p: p.age > 21)\
             .order_by(lambda p: p.name)\
             .select(lambda p: p.name)\
             .to_list()
# ผลลัพธ์: ['Alice', 'Charlie']

# ตัวอย่าง Count
# LINQ: people.Count(p => p.age < 25)
young_count = e.count(lambda p: p.age < 25) # 1 (Bob)

ไลบรารี py-linq ได้นำตัวดำเนินการคิวรีมาตรฐานส่วนใหญ่มาใช้งาน ทำให้มีอินเทอร์เฟซที่คุ้นเคยสำหรับผู้ที่เปลี่ยนมาจากหรือทำงานร่วมกับการพัฒนา .NET

ไลบรารีอื่นๆ

ไลบรารี pipe เป็นอีกทางเลือกหนึ่ง ซึ่งนำเสนอแนวทางเชิงฟังก์ชันนัลโดยใช้ตัวดำเนินการไปป์ (|) สำหรับการเชื่อมต่อการดำเนินการ ซึ่งนักพัฒนาบางคนพบว่าอ่านง่ายและแสดงออกได้ดีสำหรับกระแสข้อมูลที่ซับซ้อน

Java Streams: สิ่งเทียบเท่า LINQ มาตรฐาน

ตั้งแต่ Java 8 เป็นต้นมา สิ่งที่เทียบเท่า LINQ หลักและเป็นธรรมชาติใน Java คือ Streams API (java.util.stream) อย่างไม่ต้องสงสัย มันให้วิธีการประมวลผลลำดับขององค์ประกอบที่เป็นแบบลื่นไหล (fluent) และเชิงประกาศ ซึ่งสะท้อนปรัชญาและความสามารถของ LINQ อย่างใกล้ชิด ทำให้คุณสมบัติคล้าย LINQ กลายเป็นความจริงภายในไลบรารีมาตรฐาน

แนวคิดหลักของ Java Streams

  • แหล่งข้อมูล (Source): Streams ทำงานกับแหล่งข้อมูล เช่น Collections (list.stream()), อาร์เรย์ (Arrays.stream(array)), ช่องสัญญาณ I/O หรือฟังก์ชันสร้างข้อมูล (Stream.iterate, Stream.generate)
  • องค์ประกอบ (Elements): สตรีมแทนลำดับขององค์ประกอบ มันไม่ได้เก็บข้อมูลด้วยตัวเอง แต่ประมวลผลองค์ประกอบจากแหล่งข้อมูล
  • การดำเนินการรวมข้อมูล (Aggregate Operations): รองรับชุดการดำเนินการที่หลากหลาย เช่น filter (Where), map (Select), sorted (OrderBy), distinct, limit (Take), skip (Skip), reduce (Aggregate), collect (ToList, ToDictionary เป็นต้น)
  • การสร้างไปป์ไลน์ (Pipelining): การดำเนินการขั้นกลาง (intermediate operations) (เช่น filter, map, sorted) จะคืนค่าเป็นสตรีมใหม่ ทำให้สามารถเชื่อมต่อกันเพื่อสร้างไปป์ไลน์ที่แสดงถึงคิวรีได้
  • การวนซ้ำภายใน (Internal Iteration): แตกต่างจากการวนซ้ำคอลเลกชันด้วยลูปที่ชัดเจน (การวนซ้ำภายนอก - external iteration) ไลบรารีสตรีมจัดการกระบวนการวนซ้ำภายในเมื่อมีการเรียกใช้การดำเนินการสิ้นสุด (terminal operation)
  • ความขี้เกียจและการทำงานแบบ Short-circuiting (Laziness and Short-circuiting): การดำเนินการขั้นกลางเป็นแบบ lazy; การคำนวณจะไม่เริ่มจนกว่าการดำเนินการสิ้นสุดจะกระตุ้นให้ไปป์ไลน์ทำงาน การดำเนินการแบบ Short-circuiting (เช่น limit, anyMatch, findFirst) สามารถหยุดการประมวลผลก่อนกำหนดเมื่อได้ผลลัพธ์แล้ว ซึ่งช่วยเพิ่มประสิทธิภาพ
  • การดำเนินการสิ้นสุด (Terminal Operations): กระตุ้นการทำงานของไปป์ไลน์สตรีมและสร้างผลลัพธ์ (เช่น collect, count, sum, findFirst, anyMatch) หรือผลข้างเคียง (side-effect) (เช่น forEach)

ตัวอย่างการดำเนินการ Stream

ลองดูตัวอย่างการเทียบเท่า LINQ โดยใช้ Java Streams:

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;

// คลาสข้อมูลตัวอย่าง
class Transaction {
    int id; String type; int value;
    Transaction(int id, String type, int value) { this.id = id; this.type = type; this.value = value; }
    int getId() { return id; }
    String getType() { return type; }
    int getValue() { return value; }
    @Override public String toString() { return "ID:" + id + " Type:" + type + " Value:" + value; }
}

public class StreamExample {
    public static void main(String[] args) {
        List<Transaction> transactions = Arrays.asList(
            new Transaction(1, "GROCERY", 50),
            new Transaction(2, "UTILITY", 150),
            new Transaction(3, "GROCERY", 75),
            new Transaction(4, "RENT", 1200),
            new Transaction(5, "GROCERY", 25)
        );

        // --- การคัดกรอง (Where) ---
        // LINQ: transactions.Where(t => t.getType() == "GROCERY")
        List<Transaction> groceryTransactions = transactions.stream()
            .filter(t -> "GROCERY".equals(t.getType()))
            .collect(Collectors.toList());
        // ผลลัพธ์: ประกอบด้วย transaction ที่มี ID 1, 3, 5

        // --- การแปลงข้อมูล (Select) ---
        // LINQ: transactions.Select(t => t.getId())
        List<Integer> transactionIds = transactions.stream()
            .map(Transaction::getId) // ใช้ method reference
            .collect(Collectors.toList());
        // ผลลัพธ์: [1, 2, 3, 4, 5]

        // --- การเรียงลำดับ (OrderBy) ---
        // LINQ: transactions.OrderByDescending(t => t.getValue())
        List<Transaction> sortedByValueDesc = transactions.stream()
            .sorted(comparing(Transaction::getValue).reversed())
            .collect(Collectors.toList());
        // ผลลัพธ์: Transaction เรียงตาม value จากมากไปน้อย: [ID:4, ID:2, ID:3, ID:1, ID:5]

        // --- การรวมการดำเนินการ ---
        // ค้นหา ID ของ grocery transaction โดยเรียงตาม value จากมากไปน้อย
        // LINQ: transactions.Where(t => t.getType() == "GROCERY").OrderByDescending(t => t.getValue()).Select(t => t.getId())
        List<Integer> groceryIdsSortedByValueDesc = transactions.stream()
            .filter(t -> "GROCERY".equals(t.getType()))           // Where
            .sorted(comparing(Transaction::getValue).reversed()) // OrderByDescending
            .map(Transaction::getId)                             // Select
            .collect(Collectors.toList());                       // ประมวลผลและรวบรวม
        // ผลลัพธ์: [3, 1, 5] (ID ที่สอดคล้องกับค่า 75, 50, 25)

        // --- การดำเนินการทั่วไปอื่นๆ ---
        // AnyMatch
        // LINQ: transactions.Any(t => t.getValue() > 1000)
        boolean hasLargeTransaction = transactions.stream()
            .anyMatch(t -> t.getValue() > 1000); // true (RENT transaction)

        // เทียบเท่า FindFirst / FirstOrDefault
        // LINQ: transactions.FirstOrDefault(t => t.getType() == "UTILITY")
        Optional<Transaction> firstUtility = transactions.stream()
            .filter(t -> "UTILITY".equals(t.getType()))
            .findFirst(); // คืนค่า Optional ที่บรรจุ transaction ID:2

        firstUtility.ifPresent(t -> System.out.println("Found: " + t)); // พิมพ์ transaction ที่พบ หากมี

        // Count
        // LINQ: transactions.Count(t => t.getType() == "GROCERY")
        long groceryCount = transactions.stream()
            .filter(t -> "GROCERY".equals(t.getType()))
            .count(); // 3

        // Sum (ใช้สตรีมตัวเลขเฉพาะทางเพื่อประสิทธิภาพ)
        // LINQ: transactions.Sum(t => t.getValue())
        int totalValue = transactions.stream()
            .mapToInt(Transaction::getValue) // แปลงเป็น IntStream
            .sum(); // 1500

        System.out.println("Total value: " + totalValue);
    }
}

สตรีมแบบขนาน (Parallel Streams)

Java Streams สามารถทำให้เป็นแบบขนานได้อย่างง่ายดายเพื่อเพิ่มประสิทธิภาพการทำงานบนโปรเซสเซอร์แบบหลายคอร์ โดยเพียงแค่แทนที่ .stream() ด้วย .parallelStream() Streams API จะจัดการการแบ่งย่อยงานและการจัดการเธรดภายในเอง

// ตัวอย่าง: การคัดกรองและการแปลงข้อมูลแบบขนาน
List<Integer> parallelResult = transactions.parallelStream() // ใช้สตรีมแบบขนาน
    .filter(t -> t.getValue() > 100) // ประมวลผลแบบขนาน
    .map(Transaction::getId)         // ประมวลผลแบบขนาน
    .collect(Collectors.toList());   // รวมผลลัพธ์
// ผลลัพธ์: [2, 4] (ลำดับอาจแตกต่างจากสตรีมแบบลำดับก่อนการรวบรวม)

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

ไลบรารี Java อื่นๆ

แม้ว่า Java 8 Streams จะเป็นมาตรฐานและโดยทั่วไปเป็นสิ่งที่เทียบเท่า LINQ ที่นิยมใช้ใน Java แต่ก็มีไลบรารีอื่นๆ อยู่ด้วย:

  • jOOQ: เน้นการสร้างคิวรี SQL ที่ปลอดภัยด้านชนิดข้อมูล (type-safe) ใน Java โดยใช้ API แบบลื่นไหล เหมาะอย่างยิ่งสำหรับการดำเนินการที่เน้นฐานข้อมูลซึ่งเลียนแบบ LINQ to SQL
  • Querydsl: คล้ายกับ jOOQ นำเสนอการสร้างคิวรีที่ปลอดภัยด้านชนิดข้อมูลสำหรับระบบเบื้องหลัง (backend) ต่างๆ รวมถึง JPA, SQL และฐานข้อมูล NoSQL
  • joquery, Lambdaj: ไลบรารีรุ่นเก่าที่ให้คุณสมบัติคล้าย LINQ ซึ่งส่วนใหญ่ถูกแทนที่ด้วย Streams API ในตัวตั้งแต่ Java 8

แนวทาง C++ สำหรับคิวรีสไตล์ LINQ

C++ ไม่มีคุณสมบัติการสอบถามที่ผสานรวมกับภาษาโดยตรงที่เทียบเท่ากับ LINQ ของ .NET หรือ Streams ของ Java อย่างไรก็ตาม นักพัฒนาที่ค้นหาสิ่งเทียบเท่า LINQ ใน C++ หรือวิธีการนำรูปแบบ LINQ ไปใช้ใน C++ สามารถบรรลุผลลัพธ์ที่คล้ายกันได้โดยใช้การผสมผสานระหว่างคุณสมบัติของไลบรารีมาตรฐาน ไลบรารีบุคคลที่สามที่ทรงพลัง และสำนวน C++ สมัยใหม่

อัลกอริทึมของ Standard Template Library (STL)

เฮดเดอร์ <algorithm> และ <numeric> มีชุดเครื่องมือพื้นฐานของฟังก์ชันที่ทำงานบนช่วงของ iterator (begin, end) สิ่งเหล่านี้คือองค์ประกอบพื้นฐานสำหรับการจัดการข้อมูลใน C++

#include <vector>
#include <numeric>
#include <algorithm>
#include <iostream>
#include <string>
#include <iterator> // สำหรับ std::back_inserter

struct Product {
    int id;
    double price;
    std::string category;
};

int main() {
    std::vector<Product> products = {
        {1, 10.0, "A"}, {2, 25.0, "B"}, {3, 5.0, "A"}, {4, 30.0, "A"}
    };

    // --- การคัดกรอง (Where) ---
    // LINQ: products.Where(p => p.category == "A")
    std::vector<Product> categoryA;
    std::copy_if(products.begin(), products.end(), std::back_inserter(categoryA),
                 [](const Product& p){ return p.category == "A"; });
    // categoryA ตอนนี้ประกอบด้วย product ที่มี ID 1, 3, 4

    // --- การแปลงข้อมูล (Select) ---
    // LINQ: products.Select(p => p.price)
    std::vector<double> prices;
    prices.reserve(products.size()); // จองพื้นที่
    std::transform(products.begin(), products.end(), std::back_inserter(prices),
                   [](const Product& p){ return p.price; });
    // prices ตอนนี้ประกอบด้วย [10.0, 25.0, 5.0, 30.0]

    // --- การเรียงลำดับ (OrderBy) ---
    // LINQ: products.OrderBy(p => p.price)
    // หมายเหตุ: std::sort แก้ไข container เดิม
    std::vector<Product> sortedProducts = products; // สร้างสำเนาเพื่อเรียงลำดับ
    std::sort(sortedProducts.begin(), sortedProducts.end(),
              [](const Product& a, const Product& b){ return a.price < b.price; });
    // sortedProducts ตอนนี้คือ: [ {3, 5.0}, {1, 10.0}, {2, 25.0}, {4, 30.0} ]

    // --- การรวมข้อมูล (Sum) ---
    // LINQ: products.Where(p => p.category == "A").Sum(p => p.price)
    double sumCategoryA = std::accumulate(products.begin(), products.end(), 0.0,
        [](double current_sum, const Product& p){
            return (p.category == "A") ? current_sum + p.price : current_sum;
        });
    // sumCategoryA = 10.0 + 5.0 + 30.0 = 45.0

    // --- การค้นหา (เทียบเท่า FirstOrDefault) ---
    // LINQ: products.FirstOrDefault(p => p.id == 3)
    auto found_it = std::find_if(products.begin(), products.end(),
                                 [](const Product& p){ return p.id == 3; });
    if (found_it != products.end()) {
        std::cout << "Found product with ID 3, price: " << found_it->price << std::endl;
    } else {
        std::cout << "Product with ID 3 not found." << std::endl;
    }

    return 0;
}

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

ไลบรารีที่ใช้ Range (เช่น range-v3, C++20 std::ranges)

ไลบรารี C++ สมัยใหม่ เช่น range-v3 ของ Eric Niebler (ซึ่งมีอิทธิพลอย่างมากต่อ std::ranges มาตรฐานที่เปิดตัวใน C++20) มีไวยากรณ์ที่ประกอบได้และใช้ไปป์ (|) ซึ่งใกล้เคียงกับจิตวิญญาณของ LINQ หรือ Java Streams มากกว่า

#include <vector>
#include <string>
#include <iostream>
#ifdef USE_RANGES_V3 // กำหนดค่านี้หากใช้ range-v3 มิฉะนั้นใช้ std::ranges
#include <range/v3/all.hpp>
namespace ranges = ::ranges;
#else // สมมติว่าเป็น C++20 หรือใหม่กว่าที่รองรับ <ranges>
#include <ranges>
#include <numeric> // สำหรับ accumulate กับ ranges
namespace ranges = std::ranges;
namespace views = std::views;
#endif

// สมมติว่าใช้ struct Product จากตัวอย่างก่อนหน้า...

int main() {
    std::vector<Product> products = {
        {1, 10.0, "A"}, {2, 25.0, "B"}, {3, 5.0, "A"}, {4, 30.0, "A"}
    };

    // LINQ: products.Where(p => p.category == "A").Select(p => p.price).Sum()
    auto categoryAView = products
        | ranges::views::filter([](const Product& p){ return p.category == "A"; })
        | ranges::views::transform([](const Product& p){ return p.price; });

    #ifdef USE_RANGES_V3
        double sumCategoryA_ranges = ranges::accumulate(categoryAView, 0.0);
    #else // C++20 std::ranges ต้องการ begin/end ที่ชัดเจนสำหรับ accumulate
        double sumCategoryA_ranges = std::accumulate(categoryAView.begin(), categoryAView.end(), 0.0);
    #endif

    std::cout << "Sum Category A (ranges): " << sumCategoryA_ranges << std::endl; // 45.0

    // LINQ: products.Where(p => p.price > 15).OrderBy(p => p.id).Select(p => p.id)
    // หมายเหตุ: การเรียงลำดับด้วย ranges โดยทั่วไปต้องการการรวบรวมลงใน container ก่อน
    // หรือใช้ range actions/algorithms เฉพาะหากมีและเหมาะสม
    auto expensiveProducts = products
        | ranges::views::filter([](const Product& p){ return p.price > 15.0; });

    // รวบรวมลงใน vector เพื่อเรียงลำดับ
    std::vector<Product> expensiveVec;
    #ifdef USE_RANGES_V3
        ranges::copy(expensiveProducts, std::back_inserter(expensiveVec));
    #else
        ranges::copy(expensiveProducts.begin(), expensiveProducts.end(), std::back_inserter(expensiveVec));
    #endif

    ranges::sort(expensiveVec, [](const Product& a, const Product& b){ return a.id < b.id; }); // เรียงลำดับ vector

    auto ids_expensive_sorted = expensiveVec
        | ranges::views::transform([](const Product& p){ return p.id; }); // สร้าง view ของ ID

    std::cout << "Expensive Product IDs (Sorted): ";
    for(int id : ids_expensive_sorted) { // วนซ้ำ view สุดท้าย
        std::cout << id << " "; // 2 4
    }
    std::cout << std::endl;

    return 0;
}

ไลบรารี Range นำเสนอความสามารถในการแสดงออกที่ดีขึ้นอย่างมีนัยสำคัญ การทำงานแบบ lazy (ผ่าน views) และความสามารถในการประกอบรวมเมื่อเทียบกับอัลกอริทึม STL แบบดั้งเดิม ทำให้เป็นคู่แข่งที่แข็งแกร่งในฐานะสิ่งเทียบเท่า LINQ ใน C++

ไลบรารี LINQ เฉพาะสำหรับ C++

มีไลบรารีบุคคลที่สามหลายตัวที่มีเป้าหมายเฉพาะเพื่อเลียนแบบไวยากรณ์ LINQ โดยตรงใน C++:

  • cpplinq: ไลบรารีแบบ header-only ที่มีตัวดำเนินการ LINQ จำนวนมาก (from, where, select, orderBy เป็นต้น) พร้อมสไตล์ไวยากรณ์การเชื่อมต่อเมธอดหรือคิวรีที่คุ้นเคย
  • อื่น ๆ: โครงการต่างๆ บน GitHub นำเสนอการใช้งานสิ่งเทียบเท่า LINQ ใน C++ ที่แตกต่างกัน โดยมีความสมบูรณ์ของคุณสมบัติที่แตกต่างกันไป

ไลบรารีเหล่านี้อาจน่าสนใจสำหรับนักพัฒนาที่คุ้นเคยกับ C# LINQ อยู่แล้ว อย่างไรก็ตาม พวกมันนำมาซึ่งการพึ่งพาภายนอก (external dependencies) และอาจไม่ได้ผสานรวมเข้ากับการปฏิบัติ C++ มาตรฐานได้อย่างราบรื่นเสมอไป หรือให้ประสิทธิภาพที่อาจปรับให้เหมาะสมได้เท่ากับอัลกอริทึมมาตรฐานหรือไลบรารี range ที่เป็นที่ยอมรับ

การกล่าวถึงโดยย่อในภาษาอื่น ๆ

แนวคิดพื้นฐานของการสอบถามคอลเลกชันแบบประกาศนั้นแพร่หลาย:

  • JavaScript: JavaScript สมัยใหม่มีเมธอด Array ที่ทรงพลัง เช่น filter(), map(), reduce(), sort(), find(), some(), every() ที่ช่วยให้สามารถดำเนินการสไตล์ฟังก์ชันนัลที่เชื่อมต่อกันได้คล้ายกับ LINQ ไลบรารีเช่น lodash ให้ยูทิลิตี้ที่ครอบคลุมยิ่งขึ้นไปอีก
  • Perl: ฟังก์ชันหลัก เช่น grep (สำหรับการคัดกรอง) และ map (สำหรับการแปลงข้อมูล) ให้ความสามารถในการประมวลผลรายการที่จำเป็น
  • PHP: ฟังก์ชัน Array (array_filter, array_map, array_reduce) และไลบรารีคอลเลกชันเชิงวัตถุ (เช่น Laravel Collections, Doctrine Collections) นำเสนอคุณสมบัติการจัดการข้อมูลเชิงประกาศที่คล้ายกัน
  • ภาษาเชิงฟังก์ชันนัล (F#, Haskell, Scala): ภาษเหล่านี้มักจะมีความสามารถในการประมวลผลลำดับข้อมูลที่ทรงพลังและผสานรวมอย่างลึกซึ้งเป็นแนวคิดชั้นหนึ่ง (first-class concepts) LINQ เองก็ได้รับแรงบันดาลใจมาจากการเขียนโปรแกรมเชิงฟังก์ชันนัล F# ซึ่งเป็นภาษา .NET ยังมีไวยากรณ์นิพจน์คิวรี (query expression) ของตัวเองที่คล้ายกับของ C# มาก

สรุป

หลักการหลักของ LINQ—การสอบถามข้อมูลเชิงประกาศ การแปลงข้อมูลเชิงฟังก์ชันนัล การประเมินผลแบบ lazy และความสามารถในการประกอบรวม—ไม่ได้จำกัดอยู่แค่ใน .NET Java นำเสนอโซลูชันมาตรฐานที่แข็งแกร่งผ่าน Streams API นักพัฒนา Python ใช้ประโยชน์จาก comprehensions ในตัว โมดูล itertools และไลบรารีอย่าง py-linq โปรแกรมเมอร์ C++ สามารถใช้อัลกอริทึม STL, ไลบรารี range สมัยใหม่ (std::ranges, range-v3) หรือไลบรารีเลียนแบบ LINQ โดยเฉพาะ

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

ข่าวที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง