VB.NET LINQ와 컬렉션: .NET 10에서 직접 실행해 본 결과

VB.NET LINQ를 .NET 10에서 실행. Group By … Into·Aggregate와 메서드 구문, 지연 실행 함정, 익명 형식 Key와 동등성, XML 리터럴, List와 HashSet 조회 속도 실측.

시리즈: VB.NET 강좌 4/6 (중급). 컬렉션을 For 문으로 직접 돌리고 있는데 LINQ를 안전하게 쓰고 싶은 VB 개발자, 그리고 레거시 코드에서 VB 쿼리 구문을 처음 본 C# 개발자를 위한 글입니다. 다 읽으면 두 가지 구문으로 쿼리를 쓰고, 상황에 맞는 컬렉션을 고르고, 지연 실행과 익명 형식 동등성 때문에 생기는 버그를 피할 수 있습니다.

공공기관이나 사내 업무 시스템의 VB.NET을 유지보수하다 보면 LINQ가 이미 들어 있는 경우가 많습니다. 아무도 건드리지 않으려는 Aggregate 한 줄, C#과 전혀 다르게 생긴 Group By … Into, 연계용 XML을 만드는 XML 리터럴 같은 것들입니다. VB의 LINQ는 C# 키워드만 바꾼 것이 아닙니다. C#에 없는 절이 있고, 익명 형식의 동등성이 키워드 하나로 달라지며, 다른 .NET 언어에는 없는 XML 리터럴이 있습니다. 이 강좌는 이것들을 모두 .NET 10에서 실행해 실제 출력과, 대소문자를 구분하지 않는 VB 때문에 생긴 컴파일 오류, LINQ 대 For 루프와 List 대 HashSet 실측을 보여 줍니다.

환경: .NET SDK 10.0.403, 런타임 10.0.14, VB 언어 버전 17.13(SDK 기본값), Apple Silicon Mac (M1 Pro). 모든 파일은 Option Strict On으로 시작합니다. 시간 측정은 Release 빌드입니다.

List, Dictionary, HashSet

아래 프로그램은 각각 dotnet new console -lang VB로 만든 프로젝트의 Program.vb 한 파일입니다. 첫 프로그램은 작은 주문 목록을 씁니다.

Option Strict On

Public Class Order
    Public Property Id As Integer
    Public Property Customer As String
    Public Property City As String
    Public Property Amount As Decimal
End Class

Module Program
    Sub Main()
        Dim orders As New List(Of Order) From {
            New Order With {.Id = 1, .Customer = "Aoki", .City = "Osaka", .Amount = 1200D},
            New Order With {.Id = 2, .Customer = "Baek", .City = "Busan", .Amount = 800D},
            New Order With {.Id = 3, .Customer = "Aoki", .City = "Osaka", .Amount = 450D},
            New Order With {.Id = 4, .Customer = "Chen", .City = "Busan", .Amount = 3000D},
            New Order With {.Id = 5, .Customer = "Davis", .City = "Austin", .Amount = 150D}
        }

        Console.WriteLine("1) Dictionary / HashSet")
        Dim byId = orders.ToDictionary(Function(o) o.Id)
        Dim found As Order = Nothing
        If byId.TryGetValue(4, found) Then Console.WriteLine($"  id 4 -> {found.Customer}")
        Dim cities As New HashSet(Of String)(orders.Select(Function(o) o.City))
        Console.WriteLine($"  cities: {String.Join(", ", cities)}  Add(""Osaka"")={cities.Add("Osaka")}")

(프로그램은 다음 절로 이어집니다. 블록을 순서대로 이으면 파일 하나가 됩니다.)

1) Dictionary / HashSet
  id 4 -> Chen
  cities: Osaka, Busan, Austin  Add("Osaka")=False

HashSet.Add는 중복일 때 예외 대신 False를 돌려주므로 "이미 본 값인가?"를 싸게 확인할 수 있습니다. Dictionary는 반대로, 없는 키를 인덱서로 읽으면 예외가 납니다.

Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'banana' was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at e3.Program.Main() in ~/demo/e3/Program.vb:line 5

키가 없을 수 있으면 TryGetValue를 씁니다. Dim found As Order = Nothing의 = Nothing도 이유가 있습니다. VB에는 out 매개 변수가 없어서, 초기화하지 않으면 이런 경고가 나옵니다.

warning BC42030: 값이 할당되기 전에 'found' 변수를 참조로 전달했습니다. 런타임에 null 참조 예외가 발생할 수 있습니다.
판단 그림. 값이 있는지를 주로 묻거나 중복을 없애야 하면 HashSet(Of T). 키로 값을 찾으면 Dictionary(Of K, V) 와 TryGetValue. 순서·인덱스·중복이 필요하면 List(Of T).
가장 자주 던지는 질문으로 컬렉션을 고른다.

쿼리 구문과 메서드 구문

VB 쿼리 구문은 절이 대문자로 시작하고, C#과 달리 Select를 생략하거나 여러 필드를 바로 나열할 수 있습니다.

        Console.WriteLine("2) Query syntax")
        Dim big = From o In orders
                  Where o.Amount >= 500D
                  Order By o.Amount Descending
                  Select o.Customer, o.Amount
        For Each r In big
            Console.WriteLine($"  {r.Customer,-6}{r.Amount,8:N0}")
        Next

        Console.WriteLine("3) Method syntax, same result")
        Dim big2 = orders.Where(Function(o) o.Amount >= 500D).
                          OrderByDescending(Function(o) o.Amount).
                          Select(Function(o) New With {o.Customer, o.Amount})
        Console.WriteLine("  " & String.Join(" | ", big2.Select(Function(r) $"{r.Customer} {r.Amount:N0}")))
2) Query syntax
  Chen     3,000
  Aoki     1,200
  Baek       800
3) Method syntax, same result
  Chen 3,000 | Aoki 1,200 | Baek 800

Select o.Customer, o.Amount는 New With 없이 익명 형식을 만듭니다. C#이라면 select new { o.Customer, o.Amount }입니다. 메서드 구문 각 줄 끝의 .은 _ 없이 다음 줄로 문장을 잇기 위한 것입니다.

Group By … Into와 Aggregate

C#과 가장 많이 다른 두 절입니다. Group By … Into는 집계를 그 자리에서 이름 붙여 쓰므로, 다시 돌려야 하는 IGrouping 대신 평평한 행이 나옵니다. Aggregate … Into는 그 자리에서 실행되어 값 자체를 돌려줍니다.

        Console.WriteLine("4) Group By ... Into")
        Dim perCity = From o In orders
                      Group By o.City Into Total = Sum(o.Amount), Cnt = Count()
                      Order By Total Descending
        For Each g In perCity
            Console.WriteLine($"  {g.City,-7}{g.Cnt} orders {g.Total,8:N0}")
        Next

        Console.WriteLine("5) Aggregate")
        Dim grandTotal = Aggregate o In orders Into Sum(o.Amount)
        Dim stats = Aggregate o In orders Where o.City = "Osaka" Into Cnt = Count(), Avg = Average(o.Amount)
        Console.WriteLine($"  total={grandTotal:N0}  Osaka count={stats.Cnt} avg={stats.Avg:N0}")
4) Group By ... Into
  Busan  2 orders    3,800
  Osaka  2 orders    1,650
  Austin 1 orders      150
5) Aggregate
  total=5,600  Osaka count=2 avg=825

집계가 하나면 grandTotal은 그냥 Decimal이고, 둘 이상이면 stats는 Cnt와 Avg를 가진 익명 형식입니다. SQL의 GROUP BY … SUM()에 익숙하다면 C#의 group … by … into g보다 이쪽이 더 자연스럽게 읽힐 겁니다.

그림. 윗줄은 지연 실행 파이프라인이다. From o In orders 가 소스이고, Where 와 Order By 는 Where·OrderBy 메서드가 되며, Select 로 쿼리가 끝나지만 열거할 때까지 아무것도 실행되지 않는다. 그 대신 Group By … Into 를 쓰면 GroupBy 가 되고 Into 에 적은 집계가 붙는다. 아랫줄: Aggregate o In orders Into Sum(o.Amount) 는 그 자리에서 실행되어 Decimal 을 돌려준다.
VB 쿼리 절과 바뀌는 메서드. From 으로 시작하는 쿼리는 지연 실행되고, Aggregate … Into 는 그 자리에서 값을 돌려준다.

흔한 실수: 범위 변수와 지역 변수의 이름 충돌

이 프로그램의 첫 버전은 총합 변수 이름을 grandTotal이 아니라 total로 했고, 컴파일되지 않았습니다.

Program.vb(45,44): error BC30978: 'Total' 범위 변수가 바깥쪽 블록의 변수를 숨기거나 이전에 쿼리 식에 정의한 범위 변수를 숨깁니다.

오류 위치는 Group By 안의 Total = Sum(o.Amount)인데, 원인은 여섯 줄 뒤의 Dim total입니다. VB는 대소문자를 구분하지 않으므로 Total과 total은 같은 이름이고, 지역 변수의 범위는 선언한 줄 이전을 포함한 블록 전체입니다. Total과 total을 다른 이름으로 쓰는 습관이 있는 C# 개발자가 자주 걸립니다. 해결: 둘 중 하나의 이름을 바꿉니다. 여기서는 grandTotal로 바꿨습니다.

람다: 한 줄과 여러 줄

Function(x) 식은 값을 돌려주고 Sub(x) 문은 돌려주지 않습니다. 여러 줄 람다는 End Function / End Sub로 닫습니다.

        Console.WriteLine("6) Multi-line lambda")
        Dim label As Func(Of Order, String) =
            Function(o)
                If o.Amount >= 1000D Then Return "big"
                If o.Amount >= 500D Then Return "mid"
                Return "small"
            End Function
        Console.WriteLine("  " & String.Join(", ", orders.Select(Function(o) $"{o.Id}:{label(o)}")))
    End Sub
End Module
6) Multi-line lambda
  1:big, 2:mid, 3:small, 4:big, 5:small

VB만의 함정이 하나 있습니다. 람다 안에서 식으로 쓴 =는 비교입니다. Function(o) o.Amount = 0은 Boolean을 돌려줄 뿐 대입하지 않습니다.

지연 실행, 익명 형식의 Key, XML 리터럴

두 번째 프로그램은 요약 글에서는 경고해 주지 않는 세 가지 동작을 다룹니다.

Option Strict On
Imports System.Xml.Linq

Module Program
    Sub Main()
        Console.WriteLine("1) Deferred execution")
        Dim src As New List(Of Integer) From {1, 2, 3}
        Dim evens = From n In src Where n Mod 2 = 0 Select n
        src.Add(4)
        Console.WriteLine("  evens: " & String.Join(" ", evens))
        Dim calls = 0
        Dim q = src.Where(Function(n)
                              calls += 1
                              Return n > 0
                          End Function)
        Dim c1 = q.Count(), c2 = q.Count()
        Console.WriteLine($"  predicate calls after Count() x2: {calls}")
        Dim fixedList = q.ToList()
        src.Add(5)
        Console.WriteLine($"  ToList count={fixedList.Count}, live query count={q.Count()}")

        Console.WriteLine("2) Anonymous types and Key")
        Dim a1 = New With {.Name = "Aoki", .Age = 30}
        Dim a2 = New With {.Name = "Aoki", .Age = 30}
        Console.WriteLine($"  no Key  : Equals={a1.Equals(a2)}")
        Dim k1 = New With {Key .Name = "Aoki", .Age = 30}
        Dim k2 = New With {Key .Name = "Aoki", .Age = 99}
        Console.WriteLine($"  Key Name: Equals={k1.Equals(k2)} (Age differs: {k1.Age} vs {k2.Age})")
        k1.Age = 31
        Console.WriteLine($"  non-Key field is mutable: k1.Age={k1.Age}")
        Dim people = {New With {.Name = "Aoki", .Age = 30}, New With {.Name = "Aoki", .Age = 30}}
        Console.WriteLine($"  Distinct without Key: {people.Distinct().Count()}")
        Dim people2 = {New With {Key .Name = "Aoki", Key .Age = 30}, New With {Key .Name = "Aoki", Key .Age = 30}}
        Console.WriteLine($"  Distinct with Key   : {people2.Distinct().Count()}")
        Dim proj = From p In {("Aoki", 30), ("Aoki", 30)} Select p.Item1, p.Item2
        Console.WriteLine($"  query Select (auto Key): {proj.Distinct().Count()}")

        Console.WriteLine("3) XML literals + LINQ to XML")
        Dim items = {("Keyboard", 4800D), ("Mouse", 2200D), ("Monitor", 32000D)}
        Dim doc = <catalog version="1">
                      <%= From it In items
                          Where it.Item2 < 10000D
                          Select <item price=<%= it.Item2 %>><%= it.Item1 %></item> %>
                  </catalog>
        Console.WriteLine(doc.ToString())
        Console.WriteLine($"  first item: {doc.<item>.First().Value}, price attr: {doc.<item>(0).@price}")
        Dim total = Aggregate x In doc...<item> Into Sum(CDec(x.@price))
        Console.WriteLine($"  sum via axis: {total}")
        Console.WriteLine($"  type: {doc.GetType().FullName}, runtime: {Environment.Version}")
    End Sub
End Module
1) Deferred execution
  evens: 2 4
  predicate calls after Count() x2: 8
  ToList count=4, live query count=5
2) Anonymous types and Key
  no Key  : Equals=False
  Key Name: Equals=True (Age differs: 30 vs 99)
  non-Key field is mutable: k1.Age=31
  Distinct without Key: 2
  Distinct with Key   : 1
  query Select (auto Key): 1
3) XML literals + LINQ to XML
<catalog version="1">
  <item price="4800">Keyboard</item>
  <item price="2200">Mouse</item>
</catalog>
  first item: Keyboard, price attr: 4800
  sum via axis: 7000
  type: System.Xml.Linq.XElement, runtime: 10.0.14

지연 실행. evens를 정의할 때 리스트는 1, 2, 3이었는데 출력은 2 4였습니다. 쿼리는 String.Join이 읽을 때 실행되었기 때문입니다. Count()를 두 번 부르니 조건식이 8번(4개×2) 실행되었습니다. ToList()는 결과를 고정합니다. 5를 추가한 뒤에도 고정한 리스트는 4개, 살아 있는 쿼리는 5개를 셌습니다. 흔한 버그는 메서드 앞쪽에서 쿼리를 한 번 정의하고 아래에서 소스를 바꾼 뒤, "집계한 시점"에는 없던 행이 보고서에 섞여 들어가는 것입니다. Aggregate는 값을 돌려주며 그 자리에서 실행되므로 예외입니다.

Key. 여기가 VB에만 있는 부분입니다. C# 익명 형식은 항상 모든 속성으로 비교합니다. VB에서 New With로 만든 익명 형식은 Key를 붙인 속성만 비교하고, Key가 하나도 없으면 참조 비교가 됩니다. 그래서 똑같아 보이는 두 객체가 같지 않았고 Distinct()가 둘 다 남겼습니다. Key .Name으로 바꾸면 Age가 다른 두 객체가 같아졌습니다. Age는 무시되기 때문입니다. Key 속성은 읽기 전용이 되어 k1.Name = "Baek"은 BC30526: 'Name' 속성이 'ReadOnly'입니다.로 실패합니다. 쿼리의 Select 절이 만드는 익명 형식은 자동으로 모두 Key라서 proj.Distinct()는 1을 돌려줬습니다. C# 코드를 VB로 옮기면서 new { … }를 New With { … }로 기계적으로 바꾸면 Distinct, GroupBy, 딕셔너리 키의 결과가 조용히 달라집니다.

XML 리터럴. VB에서는 코드 안에 XML을 그대로 쓰고, <%= %>로 식이나 쿼리를 넣고, 축 속성으로 읽을 수 있습니다. .<item>은 자식 요소, .@price는 속성, ...<item>은 자손 요소입니다. 결과는 System.Xml.Linq의 평범한 XElement이고, .NET 10 콘솔 템플릿이 처음부터 참조하고 있습니다. 이 기능은 VB 9(2008년)부터 있었고, .NET 10.0.14에서도 추가 패키지 없이 컴파일·실행되었습니다.

실측: LINQ 대 For, List 대 HashSet

짝수 합계

난수 int 1,000만 개(New Random(42), 값 0~999)에서 짝수를 합합니다. 방식마다 별도 프로세스로 실행하고(dotnet run -c Release -- for), 워밍업 10회 뒤 7회의 중앙값을 냈습니다.

Option Strict On
Imports System.Diagnostics

Module Program
    ' usage: dotnet run -c Release -- for|forSorted|where|aggregate
    Sub Main(args As String())
        Dim rnd As New Random(42)
        Dim a(9_999_999) As Integer
        For i = 0 To a.Length - 1 : a(i) = rnd.Next(1000) : Next
        If args(0) = "forSorted" Then Array.Sort(a)

        Dim f As Func(Of Long)
        Select Case args(0)
            Case "for", "forSorted"
                f = Function()
                        Dim t As Long
                        For i = 0 To a.Length - 1
                            If a(i) Mod 2 = 0 Then t += a(i)
                        Next
                        Return t
                    End Function
            Case "where"
                f = Function() a.Where(Function(x) x Mod 2 = 0).Sum(Function(x) CLng(x))
            Case Else
                f = Function() Aggregate x In a Where x Mod 2 = 0 Into Sum(CLng(x))
        End Select

        For w = 1 To 10 : f() : Next                      ' warm-up (tiered JIT)
        Dim times As New List(Of Double)
        Dim result As Long
        For run = 1 To 7
            Dim sw = Stopwatch.StartNew()
            result = f()
            times.Add(sw.Elapsed.TotalMilliseconds)
        Next
        times.Sort()
        Console.WriteLine($"{args(0),-10} median {times(3),6:F1} ms  min {times(0),6:F1}  max {times(6),6:F1}  result {result}")
    End Sub
End Module
방식 프로세스 1 프로세스 2 프로세스 3
For 루프, 난수 데이터 36.3 ms 36.1 ms 34.9 ms
For 루프, 같은 데이터 정렬 8.4 ms 8.4 ms 8.3 ms
Where(…).Sum(…) 46.4 ms 45.6 ms 43.8 ms
Aggregate … Into Sum(…) 45.3 ms 45.4 ms 43.8 ms

네 방식 모두 결과는 2,494,710,708로 같았습니다.

막대그래프. 난수 배열을 For 루프로 돌리면 36.1밀리초. 같은 For 루프를 정렬된 배열에 돌리면 8.4밀리초. Where 와 Sum 은 45.6밀리초, VB Aggregate 쿼리는 45.3밀리초. 네 방식 모두 합계는 같았다.
난수 int 1,000만 개(시드 42)에서 짝수 합. 방식마다 별도 프로세스에서 워밍업 10회 후 7회 중앙값을 구하고, 프로세스 3개 중 가운데 값. Release, .NET 10.0.14, Apple Silicon Mac (M1 Pro).

알게 된 것은 세 가지입니다. Aggregate와 Where+Sum의 비용이 같으니 VB 쿼리 구문 자체의 오버헤드는 없습니다. LINQ는 루프보다 약 25% 느렸을 뿐 "몇 배"가 아니었습니다. 그리고 가장 큰 차이는 LINQ가 아니라 데이터에서 나왔습니다. 같은 데이터를 정렬만 했는데 단순 루프가 4배 빨라졌습니다. 짝수·홀수 분기를 CPU가 예측하기 쉬워졌기 때문입니다. 루프가 느리면 LINQ를 의심하기 전에 데이터부터 보세요.

저희의 첫 시도도 적어 둡니다. 모든 방식을 한 프로세스에서 차례로 쟀을 때는 어느 것을 먼저 돌렸느냐에 따라 LINQ 수치가 45~89ms 사이에서 크게 흔들렸습니다. JIT의 프로필 기반 최적화가 공유 LINQ 코드를 처음 본 람다에 맞춰 특화했기 때문이라고 추측하지만 더 파고들지는 않았습니다. 방식별로 프로세스를 나누니 흔들림이 사라졌습니다.

List.Contains 대 HashSet.Contains

짝수 100개·1만 개·10만 개를 넣은 컬렉션에 1만 번 조회(약 절반 적중). 첫 바퀴는 tiered JIT를 위해 버리고, 워밍업 20회 뒤 마지막 5회 중 최솟값입니다.

Option Strict On
Imports System.Diagnostics

Module Program
    Function CountHits(probes As Integer(), contains As Func(Of Integer, Boolean)) As Integer
        Dim hits = 0
        For Each p In probes
            If contains(p) Then hits += 1
        Next
        Return hits
    End Function

    Sub Main()
        For pass = 1 To 2                         ' pass 1 only lets tiered JIT finish
            Dim rnd As New Random(42)
            For Each size In {100, 10_000, 100_000}
                Dim list = Enumerable.Range(0, size).Select(Function(i) i * 2).ToList()
                Dim hs As New HashSet(Of Integer)(list)
                Dim probes = Enumerable.Range(0, 10_000).Select(Function(i) rnd.Next(size * 2)).ToArray()
                Dim inList As Func(Of Integer, Boolean) = AddressOf list.Contains
                Dim inSet As Func(Of Integer, Boolean) = AddressOf hs.Contains
                Dim bestList = Double.MaxValue, bestSet = Double.MaxValue, hits = 0
                For run = 1 To 25                 ' 20 warm-up runs, best of the last 5
                    Dim sw = Stopwatch.StartNew()
                    hits = CountHits(probes, inList)
                    If run > 20 Then bestList = Math.Min(bestList, sw.Elapsed.TotalMilliseconds)
                    sw.Restart()
                    If CountHits(probes, inSet) <> hits Then Throw New Exception("mismatch")
                    If run > 20 Then bestSet = Math.Min(bestSet, sw.Elapsed.TotalMilliseconds)
                Next
                If pass = 2 Then Console.WriteLine($"n={size,7:N0}: List.Contains {bestList,8:F3} ms | HashSet.Contains {bestSet,6:F3} ms | hits {hits}")
            Next
        Next
    End Sub
End Module
n=    100: List.Contains    0.126 ms | HashSet.Contains  0.089 ms | hits 5045
n= 10,000: List.Contains    6.768 ms | HashSet.Contains  0.099 ms | hits 5041
n=100,000: List.Contains   66.957 ms | HashSet.Contains  0.118 ms | hits 5025

List.Contains는 요소 수에 비례해 늘었고(요소 ×100 → 시간 ×100), HashSet.Contains는 거의 그대로였습니다. 요소 100개라면 차이를 신경 쓸 필요가 없습니다. 첫 바퀴를 버리는 처리는 장식이 아닙니다. 이것이 없던 첫 버전에서는 n=100의 HashSet이 0.43ms로 List보다 느리게 나왔습니다. .NET은 먼저 빠르게 컴파일한 tier-0 코드로 돌다가, 메서드가 한동안 자주 불린 뒤에야 백그라운드에서 최적화된 코드로 바꾸기 때문입니다. 마이크로벤치마크의 첫 줄만 이상하면 대개 이것이 원인입니다.

솔직한 한계와 버전 메모

  • .NET SDK 10.0.403 / 런타임 10.0.14, VB 언어 버전 17.13에서 컴파일·실행했습니다. 두 번째 프로그램을 옛 -p:LangVersion으로 다시 컴파일하니 9에서는 BC36716(여러 줄 람다, 컬렉션 이니셜라이저, 암시적 줄 연속, 보간 문자열, 튜플)으로 실패했지만, XML 리터럴은 오류 목록에 없었으므로 VB 9에서 컴파일됩니다. 튜플 ("Aoki", 30)에는 15가 필요합니다.
  • VB에는 새 언어 기능이 추가되지 않으므로 C#의 새 문법(컬렉션 식 등)은 VB에 없습니다. 반면 라이브러리는 별개라서 .NET 10에서 System.Linq에 추가된 메서드는 VB에서도 메서드로 호출할 수 있습니다.
  • Stopwatch로 직접 잰 값이고 BenchmarkDotNet이 아닙니다(추가 NuGet 패키지 없음). 흩어짐이 보이도록 프로세스별 값을 모두 실었습니다.
  • LINQ to Objects만 다뤘습니다. 데이터베이스용 LINQ(IQueryable)도 지연 실행은 같지만, 열거할 때마다 서버에 쿼리가 새로 나가므로 위 함정의 비용이 훨씬 큽니다.

C# 쪽 같은 주제는 C# LINQ 기초, C# LINQ group by와 join, C# 익명 형식, C# 람다식을 참고하세요.

이전: VB.NET 강좌 3: 클래스·상속·인터페이스 · 다음: VB.NET 강좌 5: Async와 Await

자주 묻는 질문

VB.NET 익명 형식의 Key 키워드는 무슨 일을 하나요?

Equals와 GetHashCode에 참여하는 것이 Key를 붙인 속성뿐이 되고, 그 속성은 읽기 전용이 됩니다. New With {.Name = "Aoki", .Age = 30}을 두 개 만들어도 같지 않아서 Distinct는 둘 다 남깁니다. Key를 붙이면 같아지고 Distinct는 1개를 돌려줍니다. 쿼리의 Select 절이 만드는 익명 형식은 자동으로 모두 Key입니다.

VB.NET XML 리터럴은 .NET 10에서도 동작하나요?

동작합니다. <%= %>로 쿼리를 넣은 XML 리터럴과 .<자식>, .@속성, ...<자손> 축 속성을 .NET 10.0.14에서 컴파일·실행했습니다. 결과는 일반 System.Xml.Linq.XElement입니다. C#에는 같은 문법이 없습니다.

HashSet.Contains는 List.Contains보다 얼마나 빠른가요?

크기에 따라 다릅니다. 1만 번 조회할 때 요소 100개짜리 List는 0.13ms, HashSet은 0.09ms였고, 요소 10만 개에서는 List가 약 67ms, HashSet이 약 0.12ms였습니다(Release, .NET 10, M1 Pro). List는 처음부터 차례로 찾고 HashSet은 해시로 찾습니다.

  • #VB.NET
  • #.NET 10
  • #LINQ
  • #컬렉션
  • #XML 리터럴
  • #중급

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑