본문으로 건너뛰기

테이블의 컬렉션 표면 — 개수 · 순회 · 첨자

상태: 구현 완료 (2026-08-28) — 개수 · 행 순회 · 쌍 순회 · 첨자 · 관련: 생성 코드의 이름 체계 · 접근자 객체화 · Lua 언어 지원

세 가지를 추가합니다.

  1. 개수 — 모든 언어에 배치합니다.
  2. 순회 — 각 언어의 반복 규약을 구현합니다. 규약이 없는 언어는 제외합니다.
  3. 첨자table[key]. 조건을 만족하는 언어에만 배치하며, 균일하지 않습니다.

셋 다 이미 공개되어 있는 멤버를 감싸는 것입니다. 읽기 경로·와이어 형식·인덱스 구축에 변경이 없고, 실행 시 비용이 추가되지 않습니다. 골든이 움직이는 이유는 생성 코드의 표면이 증가하기 때문이며 값이 달라지기 때문이 아닙니다.


1. 문제 — 공개되어 있으나 이름이 없는 것

현재 생성되는 테이블 표면은 모든 언어가 같은 구성입니다. C# 기준입니다.

public List<Record> Records // 이미 공개
public Dictionary<int, Record> RecordsByIndex // 이미 공개
public Record FindByIndex(int key) // 없으면 null
public Record GetByIndexOrThrow(int key) // 없으면 예외
public bool ContainsIndex(int key)

Go는 Records(), Rust는 records(), Python은 self.records, Lua는 self.records입니다 — 표기만 그 언어의 것이고 구성은 동일합니다.

개수도 순회도 이미 가능합니다. Records.Count이고 foreach (var r in table.Records)입니다. 부족한 것은 능력이 아니라 표기이며, 그 비용은 둘입니다.

비용내용
한 단계의 우회소비 코드가 테이블이 아니라 테이블이 보유한 목록을 먼저 이름으로 지시합니다. 「이 테이블에 몇 행인가」가 table.Records.Count가 됩니다
목록 공개가 계약이 됨RecordsList<Record>를 그대로 반환하므로 호출부가 정렬하거나 요소를 추가할 수 있습니다. 컬렉션 표면이 있으면 이후에 반환 형식을 읽기 전용으로 좁힐 여지가 생깁니다

2. 이미 존재하는 재료

주 인덱스가 어느 컬럼인지는 이미 답이 있습니다. KeyPlans.PrimaryOf(table)이 단일·복합을 가리지 않고 그 테이블의 주 키 구성을 반환합니다. 현재 이것을 사용하는 생성기는 언리얼 하나뿐이고(UnrealCodeGenerator.cs:378), 나머지 생성기의 뷰에는 주 키 정보가 실려 있지 않습니다. 첨자의 실제 작업량은 그 뷰 필드 5~6개를 각 생성기에 추가하는 부분입니다.

주 인덱스의 존재는 레이아웃이 보장합니다. CookingContext.CheckPrimaryIndexValidity가 주 키 컬럼의 타입이 키로 사용 가능한지와 필수인지를 확인하고, 어긋나면 변환 단계에서 거부합니다. 따라서 「주 인덱스가 없는 테이블」은 생성기에 도달하지 않습니다.

다만 단일 인덱스가 하나도 없는 테이블은 존재합니다. 복합 기본키만 선언한 테이블이 그렇습니다 — 픽스처 composite-keyBeastMove(BeastId, MoveId) 복합키 하나만 보유하며, 생성된 C#에는 단일 컬럼 인덱스 구역이 없습니다. §5.4의 근거입니다.


3. 개수

모든 언어에 배치합니다. 이름은 각 언어의 컬렉션 관용 표기를 따릅니다.

언어표기
C#Count
C++size() · empty()
C추가하지 않습니다table.count가 이미 구조체 멤버입니다
GoCount()
Javasize()
Kotlinsize
Swiftcount
Rustlen() · is_empty()
Dartlength
TypeScriptcount
Python__len__
Rubysize
PHPCountable 구현 — count($table)
Lua:count()
언리얼Num()

Rust는 len()is_empty()를 짝으로 배치합니다. clippy의 len_without_is_empty가 한쪽만 있는 것을 거부하고, 툴체인 게이트가 경고를 오류로 처리합니다.

Lua는 # 연산자를 사용하지 않습니다. 생성 코드는 LuaJIT 2.1과 Lua 5.3+ 양쪽에서 동일하게 동작해야 하는데, LuaJIT 2.1은 테이블의 __len 메타메소드를 호출하지 않습니다. #table이 한쪽 런타임에서만 정상 값을 반환하는 상태가 되므로, 메소드로 배치합니다.

3.1 컬럼 이름과의 충돌

충돌하지 않습니다. 근거는 컬럼 이름이 테이블 쪽에 없다는 것이 아니라 — FindByIndex는 명백히 컬럼에서 생성된 테이블 멤버입니다 — 컬럼에서 생성되는 테이블 스코프 이름에는 예외 없이 접두어가 붙는다는 것입니다.

csharp-table.sbn의 테이블 스코프 선언 전체입니다.

이름컬럼 유래
Record · FieldNames · BuildObjectValueMap · Records · ReadAsync · ToString고정
RecordsBy<X> · FindBy<X> · GetBy<X>OrThrow · Contains<X> · KeyOf<X>접두어 + 컬럼

따라서 count 컬럼이 생성하는 테이블 멤버는 RecordsByCount · FindByCount · GetByCountOrThrow · ContainsCount이고, 맨 이름 Count는 어떤 컬럼에서도 생성되지 않습니다. GetEnumerator와 첨자도 같습니다.

중첩 타입도 테이블 스코프에 없습니다. <Pascal>Entry 구조체는 컬럼 이름에서 생성되지만 Record 안에 배치됩니다(polymorphismSkillTable.cs에서 EffectEntryRecord의 범위 안에 있습니다). 테이블 스코프의 유일한 중첩 타입은 Record입니다.

이 성질은 모든 언어에서 동일합니다 — Go의 byIndex, Python의 by_index, Lua의 byIndex, Ruby의 find_by_*가 전부 접두어를 보유합니다.

컴파일되지는 않으나 읽기에 혼동이 생기는 자리가 하나 있습니다. count 컬럼이 있는 테이블에서는 table.Count(행 수)와 table.Records[0].Count(그 행의 컬럼 값)가 공존합니다. 서로 다른 타입의 멤버이므로 오류가 되지 않지만, 두 이름이 같은 화면에 나타납니다. 이것은 접두어 규칙으로 방지할 수 없고, Records.Count를 사용하던 현재도 동일하게 존재하는 상태입니다.

3.2 반복 규약이 주입하는 이름

§4의 Ruby Enumerable과 Swift Sequence한 번의 선언으로 다수의 이름을 테이블 클래스에 추가합니다 — Ruby는 count · find · first · min · sort 등, Swift는 map · filter · first(where:) 등입니다.

이 이름들도 위 접두어 규칙 덕분에 컬럼 유래 멤버와 충돌하지 않습니다. 충돌하는 것은 컬럼 이름이 아니라 생성 코드가 이미 호출하고 있던 전역 함수입니다.

Swift에서 실제로 발생하였습니다(2026-08-28 실측). Sequence 준수가 Sequence.max()를 클래스 본문의 이름 공간에 넣고, 그것이 전역 Swift.max(_:_:)를 가립니다. 읽기 경로가 배열 길이를 max(0, ...)로 계산하고 있었으므로 준수를 추가한 순간 그 줄이 컴파일되지 않았습니다.

error: use of 'max' refers to instance method rather than global function 'max'

수정은 Swift.max(0, ...)로 한정하는 것이고, 템플릿에 그 이유를 주석으로 남겼습니다. 같은 성질의 이름이 min에도 있습니다.

성질내용
검출 경로컴파일 오류입니다. Swift·Kotlin·Java·Rust에서는 게이트가 검출합니다
검출되지 않는 언어Ruby입니다. Enumerable의 이름을 테이블이 이미 정의하고 있으면 오류가 아니라 재정의가 되고, 그 재정의는 정상 동작처럼 보입니다
추가할 때 확인할 것반복 규약을 새로 준수시키는 언어에서는 그 규약이 주입하는 이름 목록을 생성 코드가 호출하는 이름과 대조합니다

4. 순회

언어형태
C#GetEnumerator() — §4.1
C++ · 언리얼begin() · end() 위임
Javaimplements Iterable<Record>
Kotlinoperator fun iterator()
SwiftSequence 준수
Rustimpl IntoIterator for &Table · iter()
DartIterable<Record> 구현
TypeScript[Symbol.iterator]()
Python__iter__
Rubyinclude Enumerable · each
PHPIteratorAggregate
Lua:iter() 반복자 함수
Goiter.Seq — §4.3
C추가하지 않습니다 — §4.4

부수 효과가 큰 언어가 둘 있습니다. Swift는 Sequence 준수만으로 map · filter · first(where:)가 함께 사용 가능해지고, Ruby는 Enumerable 포함만으로 같은 결과가 됩니다. 두 언어에서는 이 변경이 표기 개선을 넘어 표준 라이브러리 전체와의 연결이 됩니다.

4.1 C# — 구조체 열거자

IEnumerable<Record>만 구현하면 foreach 한 번마다 열거자가 박싱되어 힙 할당이 발생합니다. 생성 코드는 유니티 타깃이고([System.Serializable] · TabbitUnityAdapter), 매 프레임 순회하는 호출부에서 이 할당이 누적됩니다.

따라서 List<Record>.Enumerator를 반환하는 공개 GetEnumerator()를 먼저 배치하고, IEnumerable<Record>IEnumerable은 명시적 인터페이스 구현으로 병기합니다. foreach는 덕 타이핑으로 구조체 열거자를 선택하므로 할당이 없고, LINQ는 인터페이스 경로를 사용합니다.

GetEnumerator()_records한 번만 읽습니다. 갱신이 목록 참조를 통째로 교체하는 방식이므로(Thread.MemoryBarrier 이후 _records = records), 참조를 한 번 취득한 순회는 갱신 중에도 일관된 행 집합을 유지합니다. 이것은 Records의 현재 주석이 이미 명시하고 있는 성질이며, 새로 도입되는 보장이 아닙니다.

4.2 키와 행의 쌍 — 이름 붙은 뷰

기본 순회는 행을 반환하고, 쌍은 별도의 뷰로 배치합니다. dict가 키를 순회하고 dict.items()가 쌍을 반환하는 것과 같은 구조입니다.

쌍이 필요한 이유

주 키 컬럼의 이름이 테이블마다 다릅니다. 픽스처에서만 Index · Code · BeastId · Serial이 각각 주 키이고, 시트가 이름을 지정하면 무엇이든 될 수 있습니다 (Table.PrimaryIndexName). 따라서 행에서 키를 읽는 코드는 테이블마다 컬럼 이름을 기억해야 합니다.

foreach (var row in items) { Use(row.Index, row); } // 테이블마다 이름이 다름
foreach (var (key, row) in items.Entries) { Use(key, row); } // 모든 테이블에서 동일

키 값 자체는 행에 이미 있으므로 쌍은 데이터를 추가하지 않습니다. 추가하는 것은 어느 컬럼이 키인지를 소비 코드가 알지 않아도 되는 성질입니다.

기본 순회를 쌍으로 하지 않는 이유

비용내용
표준 라이브러리와의 연결이 나빠집니다Swift Sequence · Ruby Enumerable · C# LINQ가 전부 쌍 위에서 동작하게 됩니다. table.map { $0.name }table.map { $0.row.name }이 되고, 가장 흔한 「행만 순회」가 매번 한 단계를 경유합니다
테이블은 근본적으로 순서 있는 행 목록입니다파일이 기록한 순서가 있고 그 순서에 의미가 있습니다. 사전이 아니므로 기본 형태가 사전의 것일 이유가 없습니다
복합 기본키에서 성립하지 않습니다아래 참조

따라서 둘을 함께 배치합니다. 기본 순회는 행, Entries는 쌍입니다.

언어별 형태

이름은 각 언어가 사전에 사용하는 낱말을 따릅니다.

언어표기
C#EntriesIEnumerable<(TKey Key, Record Row)>
KotlinentriesSequence<Pair<K, Record>>
Javaentries()Iterable<Map.Entry<K, Record>>
SwiftentriesSequence, 원소가 (key:row:)
DartentriesIterable<MapEntry<K, Record>>
TypeScriptentries()
GoEntries() iter.Seq2[K, *Record]
Rustentries()impl Iterator<Item = (&K, &Record)>
Pythonitems()
Rubyeach_pair
PHPentries() — 제너레이터
Lua:entries()
C++ · C · 언리얼배치하지 않습니다 — C++17에는 범위 뷰가 없어 별도 타입을 작성해야 하고, 얻는 것에 비해 생성 코드가 증가합니다

순서와 출처

쌍은 인덱스 사전이 아니라 행 목록에서 생성합니다. 각 행에서 주 키 컬럼을 읽어 쌍으로 반환합니다.

이유내용
순서 보존사전 순회는 순서가 정의되지 않습니다. 행 목록은 파일이 기록한 순서를 유지하므로, 기본 순회와 Entries가 같은 순서가 됩니다
비용 없음추가 자료구조가 없고, 키는 이미 행 안에 있습니다

C#의 할당

이터레이터 메소드(yield return)는 열거자를 힙에 할당합니다. §4.1이 기본 순회에서 할당을 제거하는 것과 어긋나므로, Entries도 구조체 열거자를 반환하는 전용 타입으로 작성합니다. 그러지 않으면 「기본 순회는 할당이 없고 쌍 순회는 있다」가 되어 호출부가 어느 쪽인지 기억해야 합니다.

구현은 자기 자신을 열거하는 구조체입니다 — GetEnumerator()this를 반환하므로 컬렉션 타입과 열거자 타입이 하나이고, 생성되는 줄이 12줄입니다. 원소는 이름 있는 ValueTuple이라 foreach (var (key, row) in table.Entries)가 별도의 Deconstruct 없이 분해됩니다.

public EntryEnumerator GetEnumerator() => this;
public bool MoveNext() => ++_at < _rows.Count;
public (int Key, Record Row) Current => (_rows[_at].Index, _rows[_at]);

주 키의 판정

KeyPlan.IsPrimary가 정합니다. 생성기 10개는 KeyPlans.Of(table)를 순회하므로 그 값을 인덱스 뷰에 그대로 싣고, C#과 TypeScript는 SerialFields를 직접 거르므로 KeyPlans.PrimarySingleName(table)과 컬럼 이름을 대조합니다. 어느 쪽이든 판정은 한 곳에 있고, 「단일 컬럼인가」와 「주 키인가」를 생성기마다 다시 계산하지 않습니다.

복합 기본키 테이블

쌍 순회를 생성하지 않습니다. 복합 키에는 단일한 키 값이 없고, 언어별 n-튜플 지원이 갈립니다(Kotlin의 Pair · Triple은 3개까지입니다). 그 테이블에서는 행이 이미 모든 구성 컬럼을 각자의 이름으로 보유하므로, 기본 순회로 충분합니다.

§5.4와 같은 성질입니다 — 같은 언어 안에서도 테이블에 따라 유무가 분기하므로, 생성 코드가 그 사실을 주석으로 명시해야 합니다.

4.3 Go — 생성되는 go.mod의 버전 상향

iter.Seq는 Go 1.23부터이고, 현재 생성되는 go.modgo 1.21입니다 (GoCodeGenerator.GoVersion의 기본값). 기본값을 1.23으로 상향합니다.

GoVersion은 레시피가 재정의할 수 있는 옵션이므로, 1.23 미만으로 설정된 레시피에서는 반복자를 생성하지 않습니다. 그 경우 for _, r := range t.Records()가 그대로 유지되며, 생성 결과가 현재와 동일합니다.

판단내용
이득Go만 순회 표기에서 제외되는 상태를 방지합니다
비용Go 1.21·1.22 툴체인을 사용하는 소비 프로젝트는 GoVersion을 명시해야 합니다. 명시하지 않으면 go build가 버전 요구로 실패합니다
완화옵션이 이미 존재하므로 완화 경로가 레시피 한 줄입니다

4.4 C — 대상 제외

C에는 반복 규약이 없습니다. for (i = 0; i < t.count; i++)가 이미 관용구이고, 매크로를 도입하면 생성 코드에 이름 하나가 증가하는 대신 얻는 것이 없습니다.


5. 첨자 — 균일하지 않은 유일한 항목

조건 셋을 모두 만족하는 언어에만 배치합니다.

  1. 그 언어에 첨자 연산자가 있습니다.
  2. 그 테이블의 주 인덱스가 단일 컬럼입니다.
  3. 첨자 연산자가 그 키 타입을 인자로 받을 수 있습니다.
언어판정형태없는 키
C#배치this[TKey key] · 복합 가능예외
Python배치__getitem__ · 복합은 튜플예외
C++배치operator[] — 단일 인자예외
Kotlin배치operator fun get · 복합 가능null
Swift배치subscript · 복합 가능nil
Ruby배치[] · 복합 가능nil
Dart배치operator [] — 단일 인자null
PHP배치ArrayAccess — 단일 인자null
Rust제외§5.3
TypeScript제외§5.2
Go · Java · C제외연산자 재정의가 없습니다
Lua제외§5.5
언리얼제외C++ 문법상 가능하나 UE 코드 규약이 명시적 함수를 사용합니다

PHP만 첨자가 인터페이스입니다. ArrayAccess를 선언하고 네 메소드 중 하나라도 없으면 클래스가 적재되지 않으므로, 클래스 선언 시점에 「첨자가 생성되는가」를 알아야 합니다. PhpTableView.HasKeySubscript가 그것입니다. 나머지 언어는 첨자가 평범한 멤버라 인덱스 반복 안에서 판정하면 됩니다.

쓰기 첨자는 없습니다. PHP의 offsetSet · offsetUnset은 인터페이스가 요구하므로 존재하지만 LogicException을 던집니다 — 행은 파일에서 오는 것이고 대입으로 생기는 것이 아닙니다.

5.1 이득과 비용

구분내용
이득가장 빈번한 호출인 주 키 조회가 최단 표기가 됩니다. 사전·배열을 읽는 코드와 같은 모양이 되므로 소비 코드의 읽기 비용이 낮아집니다
비용 1표면의 균일성이 무너집니다. 현재 생성 코드의 주석은 「Tabbit이 생성하는 모든 언어가 이것을 같은 이름으로 보유합니다」를 FindByX에 대해 명시하고 있습니다. 첨자는 그 문장이 성립하지 않는 첫 항목이 됩니다
비용 2언어를 옮겨 적는 코드가 변환되지 않습니다. 같은 데이터를 C#과 TypeScript 양쪽에서 읽는 프로젝트에서 한쪽만 첨자를 사용할 수 있습니다
비용 3§5.2~§5.5의 언어별 위험

균일성 상실이 이 절의 핵심 비용입니다. 개수와 순회는 그 언어의 관용 표기로 이름만 달라지고 능력은 모든 언어에 존재하지만, 첨자는 능력 자체가 존재하지 않는 언어가 있습니다. 문서에 「이 언어에는 없습니다」를 기재해야 하고, 그 기재가 낡지 않도록 언어를 추가할 때마다 판정해야 합니다.

5.2 TypeScript에서 불가능한 이유

TypeScript의 인덱스 시그니처는 클래스의 모든 멤버가 그 타입에 부합할 것을 요구합니다. [key: string]: Record를 선언하면 records · findByIndex · read가 전부 Record 타입이어야 하므로 컴파일되지 않습니다. 우회 수단은 Proxy 뿐이고, 그것은 모든 속성 접근에 함수 호출을 삽입하므로 채택하지 않습니다.

첨자가 가장 자연스러워 보이는 언어에서 불가능하다는 점이 이 설계의 약점입니다.

5.3 Rust — 보류

Index 트레이트는 Option을 반환할 수 없습니다. 반환 타입이 &Record이므로 키가 없으면 패닉만 가능합니다. 현재 Rust 생성 코드는 find_by_indexOption, get_by_index_or_errorResult이며 패닉하는 경로가 하나도 없습니다. 첨자를 추가하면 그 성질이 깨집니다.

선택지평가
배치하지 않음현재 성질이 유지됩니다. Rust 사용자는 find_by_index를 계속 사용합니다
패닉으로 배치표기는 짧아지나 「이 크레이트는 패닉하지 않습니다」를 더 이상 말할 수 없게 됩니다

배치하지 않는 쪽을 권고합니다. Rust에서 &map[key]가 패닉하는 것은 표준 라이브러리와 동일한 동작이므로 놀라움은 작지만, 데이터 파일에서 읽은 키로 패닉하는 경로가 라이브러리에 생기는 것은 이득에 비해 대가가 큽니다.

5.4 복합 기본키 테이블

복합 기본키만 보유한 테이블은 실재합니다(§2). 그 테이블의 첨자는 인자가 2개 이상이어야 합니다.

언어다중 인자 첨자
C#this[int a, Slot b] — 가능
Kotlinoperator fun get(a, b) — 가능
Swiftsubscript(_:_:) — 가능
Pythont[a, b] — 튜플로 가능
Ruby[]에 인자 2개 — 가능
C++불가 — 다중 인자 첨자는 C++23이고 툴체인 게이트가 -std=c++17입니다
Dart · PHP불가 — 인자 1개 고정

따라서 복합 기본키 테이블의 첨자는 C# · Kotlin · Swift · Python · Ruby에만 생성됩니다. 같은 언어 안에서도 테이블에 따라 첨자의 유무가 분기하게 되므로, 생성 코드가 그 사실을 주석으로 명시합니다.

픽스처가 그 분기를 실제로 담고 있습니다. composite-key 시나리오의 테이블 8개 중 4개가 복합 기본키입니다 — C++ · Dart · PHP에서는 그 4개에 첨자가 없고 나머지 4개에는 있습니다.

5.5 Lua — 대상 제외

Lua에서 첨자는 __index 메타메소드이고, 그 자리는 이미 메소드 디스패치가 사용하고 있습니다. 현재 인스턴스는 __index = AssetTable로 설정되어 있어 메소드 조회가 단일 테이블 참조입니다. 키 조회를 함께 처리하려면 __index를 함수로 변경해야 하고, 그러면 모든 메소드 호출이 함수 호출 한 번을 추가로 경유합니다. 표기 하나를 위해 그 테이블의 모든 호출에 비용을 부과하는 교환이므로 채택하지 않습니다.

5.6 int 키의 서수 오해 — 가장 큰 위험

주 인덱스가 int인 테이블에서 items[0]「0번째 행」으로 읽히지만 실제로는 「Index가 0인 행」입니다. 서수 첨자와 키 첨자를 같은 기호가 담당하게 되고, 두 해석이 모두 타입에 부합하므로 컴파일러가 검출하지 못합니다.

성질내용
증상잘못된 행을 조용히 읽거나, 없는 키로 예외가 발생합니다. 전자가 더 위험합니다
영향 범위주 인덱스가 int인 테이블. 실제 시트에서 가장 흔한 형태입니다
안전한 경우키가 string · UUID · enum이면 서수로 읽힐 여지가 없습니다

선택지는 셋입니다.

내용평가
A. 전부 배치int 키에도 첨자를 생성표기는 일관되나 오해 위험이 남습니다
B. 비정수 키에만 배치string · UUID · enum 키의 테이블에만 생성위험이 제거되나 같은 언어 안에서 테이블마다 첨자 유무가 분기하고, 그 분기 기준이 시트의 키 타입이므로 소비 코드에서 예측하기 어렵습니다
C. 순회를 배열처럼 보이지 않게 유지테이블 자신은 첨자만 보유하고 서수 접근은 Records로만 허용현재 상태가 이미 그러합니다

A와 C의 조합을 권고합니다. 첨자는 키 조회로 통일하고, 서수 접근은 Records 경로에만 유지합니다. 테이블 자체는 목록 형태로 첨자되지 않으므로 「이 테이블의 첨자는 키다」가 언어 안에서 하나의 규칙이 됩니다. B는 위험은 제거하지만 예측 가능성을 더 크게 손상시킵니다.

5.7 키가 없을 때의 동작

그 언어의 사전 첨자와 같게 동작합니다. 2026-08-28 결정.

이 절은 「전 언어 일괄 예외」로 적혀 있었고, 근거는 「Dictionary·List의 첨자와 동일한 관례」였습니다. 구현 직전 확인에서 그 전제가 사실이 아니었습니다 — 사전 첨자가 예외인 언어는 대상 8개 중 3개뿐입니다.

언어그 언어의 사전 첨자생성되는 첨자
C#Dictionary[k] → 예외GetByXOrThrow 위임
Pythondict[k]KeyErrorget_by_x_or_throw 위임
C++map::at() → 예외. const 테이블에서는 operator[]의 삽입 동작이 성립하지 않습니다get_by_x_or_throw 위임
KotlinMap[k]V?findByX 위임
SwiftDictionary[k]V?findByX 위임
RubyHash#[]nilfind_by_x 위임
DartMap[]V?findByX 위임
PHP$arr[$k] → 경고와 nullfindByX 위임

채택 근거

첨자를 만드는 이유가 「그 언어의 내장 인덱싱처럼 보이는 것」입니다. 그렇게 보이면서 다르게 동작하면 이름 붙은 메소드보다 나쁩니다 — 이름이 있으면 최소한 다르다는 신호가 있습니다.

널 반환이 정보를 숨기지 않습니다. 널 가능 5개 언어는 전부 타입 검사기가 확인을 강제합니다(PHP만 예외이고, 거기서는 사전 첨자 자체가 그렇습니다). 「호출부에서 다음 줄이 널 검사인지 예외 처리인지 보이지 않는다」는 FindByX·GetByXOrThrow 분리의 근거는 타입이 답하지 않는 언어에서만 성립하는데, 그 언어가 C#·Python·C++이고 그쪽은 예외를 씁니다.

비용내용
table[k]의 의미가 언어마다 다릅니다§5.1의 균일성 비용이 존재 여부에 더해 의미까지 갈리는 것으로 확대됩니다. 문서에 언어별로 명시해야 합니다
Swift의 get throws 회피일괄 예외였다면 Swift 첨자가 get throws가 되어 호출부가 try table[k]가 됩니다. 이 결정이 그것을 없앱니다

어느 쪽이든 첨자는 FindByX를 대체하지 않습니다. 예외 언어에서는 없을 수 있는 키가 FindByX이고, 널 언어에서는 있어야 하는 키가 GetByXOrThrow입니다.


6. 언어별 최종 판정

쌍 순회는 주 키가 단일 컬럼인 테이블에만 생성됩니다(§4.2).

언어개수행 순회쌍 순회첨자
C#CountGetEnumerator()Entriesthis[key]
C++size() · empty()begin() · end()배치하지 않음operator[] — 단일 키만
C이미 존재해당 없음해당 없음해당 없음
GoCount()iter.Seq — 1.23 이상Entries()iter.Seq2해당 없음
Javasize()Iterableentries()해당 없음
Kotlinsizeiterator()entriesget — 복합 키 포함
SwiftcountSequenceentriessubscript — 복합 키 포함
Rustlen() · is_empty()IntoIteratorentries()보류(§5.3)
DartlengthIterableentriesoperator [] — 단일 키만
TypeScriptcountSymbol.iteratorentries()불가(§5.2)
Python__len____iter__items()__getitem__ — 복합 키 포함
RubysizeEnumerableeach_pair[] — 복합 키 포함
PHPCountableIteratorAggregateentries()ArrayAccess — 단일 키만
Lua:count():iter():entries()배치하지 않음(§5.5)
언리얼Num()begin() · end()배치하지 않음배치하지 않음

HTML · JSON · CSV · TSV · XML은 코드가 아니므로 해당하지 않습니다.


7. 단계

셋으로 분리합니다. 의존 관계가 그렇게 갈립니다 — 1단계는 템플릿만으로 성립하고, 2·3단계는 각 생성기의 뷰에 주 키 정보를 추가해야 합니다.

단계범위성질
1단계개수 · 행 순회모든 언어에 균일합니다. 설계 결정이 필요 없고 §5의 위험이 전부 해당하지 않습니다. 템플릿 수정이 대부분이고, Go만 생성기에 버전 판정이 추가됩니다. 구현 완료
2단계쌍 순회주 키 정보가 뷰에 필요합니다. 복합 기본키 테이블은 제외되므로 같은 언어 안에서 유무가 분기합니다. 구현 완료
3단계첨자균일하지 않습니다. §5.6은 A+C, §5.7은 그 언어의 사전을 따르는 쪽으로 결정하였습니다. 구현 완료

2단계와 3단계는 같은 뷰 작업을 공유합니다. 주 키 필드를 각 생성기에 추가하는 부분이 동일하므로, 3단계를 채택한다면 2단계에서 추가한 것을 그대로 사용합니다.

7.1 작업 범위

작업범위
템플릿 수정src/templates/*-table.sbn
뷰·생성기 수정2·3단계에 필요합니다. 각 생성기의 테이블 뷰에 주 키 필드 5~6개를 추가합니다(언리얼은 이미 보유). 1단계에는 필요하지 않습니다
Go 옵션GoCodeGenerator.GoVersion 기본값 상향
골든 재기록TABBIT_UPDATE_GOLDEN=1. 시나리오 전체 × 언어 전체
샘플 재생성samples/*/out/. 게이트가 없으므로 이 단계가 누락되기 쉽습니다
전체 스위트생성 코드를 변경하므로 언어 툴체인 게이트가 실제로 동작해야 합니다. 골든만으로는 부족합니다

절차의 상세는 아키텍처와 개발에 있습니다.

7.2 되돌리기

1단계는 되돌리기가 용이합니다 — 템플릿에서 추가한 구역을 제거하고 골든을 재기록하면 원래 산출물과 바이트 단위로 동일해집니다. 2·3단계는 생성기 뷰에 필드가 추가되므로 되돌릴 때 그쪽도 함께 제거해야 합니다.


8. 미결

항목내용
§5.6int 키 첨자의 채택 여부. A+C 조합을 권고하나 확정되지 않았습니다
§5.3Rust 첨자. 배치하지 않는 쪽을 권고합니다
Records의 반환 형식컬렉션 표면이 배치된 뒤 읽기 전용 형식으로 좁힐 것인지. 좁히면 소비 코드가 깨지므로 별도 판단이 필요합니다
이름C#의 CountRecords.Count와 항상 같은 값인지를 문서가 명시할 것인지. 현재 설계에서는 같습니다
§4.2쌍 뷰의 이름. 언어별 관용을 따르는 쪽으로 구현하였습니다(Entries · entries · items · each_pair). 전 언어 단일 이름으로 통일할 것인지가 남아 있습니다