원소가 없을 수 있는 배열 — T?[] · T[]? · T?[]?
상태: 구현됨 — 표기·모델·검증,
json, 형식 v106, 그리고 모든 언어의 리더와 생성기까지
배열의 원소 하나가 값을 갖지 않는 것을 시트가 적고 파일이 담을 수 있게 하는 설계입니다.
지금은 배열 전체만 옵셔널입니다. 옵셔널 필드가 ?를 배열 괄호 뒤에만
두기로 정하였고, 그때의 근거는 「한 셀에 든 목록의 원소는 다 있거나 다 없거나」였습니다. 그
전제가 실제 시트에서 성립하지 않는 자리가 셋 있고, 이 문서는 그 셋을 하나의 표기로 해소합니다.
1. 문제 — 원소의 없음을 적을 자리
| 자리 | 지금 일어나는 일 |
|---|---|
| 가운데가 빈 연번 배열 | Slot1·Slot3만 채운 로우는 거부됩니다. 가운데 원소가 타입의 빈 값이 되고, 읽는 쪽이 「없음」과 「0」을 구별할 수 없기 때문입니다. 완화하려면 AllowArrayGaps로 구별을 포기하는 수밖에 없습니다 |
엇갈린 ? | Layer[0]은 필수, Layer[1]은 옵셔널로 적은 시트에서 첫 컬럼이 전부를 대표합니다. 원소마다 다르게 적을 방법이 없어서 내린 결정입니다 |
구분자 셀의 - | 10;-;30은 오류입니다 (빈 칸과 없음). 원소가 없음을 가질 수 없으므로 그 자리에서 거부합니다 |
셋 다 같은 빈칸을 다르게 우회한 것입니다. 원소의 없음을 표현할 수 있으면 셋이 함께 없어집니다.
2. 표기 — ?의 자리와 그 뜻
| 타입 칸 | 배열 자체 | 원소 |
|---|---|---|
int[] | 필수 | 필수 |
int[]? | 없을 수 있습니다 | 필수 |
int?[] | 필수 | 없을 수 있습니다 |
int?[]? | 없을 수 있습니다 | 없을 수 있습니다 |
C#의 읽기와 같습니다. int?[]는 원소가 nullable인 배열이고, int[]?는 배열 변수가
nullable이며, int?[]?는 둘 다입니다. 생성 코드를 받는 언어들이 ?로 뜻하는 것과 같게
읽히는 것이 ?를 타입에 붙인 이유였고, 원소 쪽도
같은 이유로 같은 자리에 붙입니다.
괄호 앞의 ?는 원소, 뒤의 ?는 배열입니다. 파서는 이름을 뒤에서부터 읽습니다 — 끝의
?를 떼면 배열의 답이고, 남은 int?[]에서 괄호를 떼고 다시 끝의 ?를 떼면 원소의 답입니다.
?int[]처럼 앞에 붙인 것은 거부하고, 그 자리에서 두 표기를 함께 냅니다.
required가 계속 기본입니다. ?를 적지 않은 컬럼은 지금과 완전히 같으므로, 이 기능은
존재하는 모든 시트에 대해 더해지기만 합니다.
3. 셀에 적는 법 — 없음은 여기서도 -
원소의 없음도 -입니다. 빈 칸과 없음이 정한 것을 원소 자리에
그대로 적용합니다 — 표기가 자리마다 다르면 외울 것이 하나 더 생깁니다.
| 배열 셀 | int[] | int?[] |
|---|---|---|
10;20;30 | [10, 20, 30] | [10, 20, 30] |
10;-;30 | 오류 | [10, null, 30] |
10;;30 | 오류 — int는 빈 칸을 읽지 못합니다 | 오류 — 같습니다 |
- | 오 류 — int[]?가 아닙니다 | 배열 전체의 없음. int?[]?일 때만 |
| (빈 칸) | 빈 배열 | 빈 배열 |
빈 원소는 없음이 아닙니다. ?가 붙어도 그렇습니다. 셀 문법에서 빈 칸이 타입의 빈 값이듯,
빈 원소도 그 타입이 빈 칸을 읽는 방법 그대로입니다 — string[]의 a;;b는 가운데가 빈
문자열이고, 그것은 값입니다.
실측이 이것을 요구합니다. 이전 대규모 코퍼스의 산출물에는 빈 문자열 원소를 가진 배열이 5,200개 있습니다. 빈 원소를 없음으로 읽거나 오류로 만들면 그 5,200개가 전부 다른 것이 되고, 그 변화는 조용합니다. 없음은 적은 것이어야 합니다.
연번 컬럼 그룹 — 컬럼의 ?는 원소의 답
컬럼 여러 개가 배열 하나로 접히는 그룹(Tag1·Tag2·Tag3)에는 배열을 가리키는 타입 칸이
없습니다. 칸마다 적혀 있는 것은 원소 하나의 타입이고, 배열은 접으면서 생깁니다. 그래서
T?[]를 적을 자리가 없고, 대신 그 칸의 ?가 그 원소의 답입니다.
Tag1 | Tag2 | Tag3 | string | string? |
|---|---|---|---|---|
a | b | c | ["a","b","c"] | ["a","b","c"] |
a | - | c | 오류 — 원소가 없음을 가질 수 없습니다 | ["a", null, "c"] |
- | b | c | 오류 | [null, "b", "c"] |
a | (빈 칸) | c | ["a","","c"] | ["a","","c"] |
배열 전체의 없음은 연번 그룹에 없습니다. 그 컬럼들은 모든 로우에 존재하므로 「배열이 없다」가
뜻할 수 있는 것은 「원소가 전부 없다」뿐이고, 그것은 위 표가 원소마다 적습니다. 자르기를 켠
테이블에서 그 로우는 길이 0, 즉 []입니다. 정말 「이 배열 자체가 없을 수 있다」가 필요하면
구분자 배열(T[]?)이 그것입니다 — 거기서는 셀 하나가 배열 하나에 대응합니다.
그 읽기가 하고 있던 일
첫 컬럼의 ?를 배열 전체의 것으로 읽는 동안, 배열의 존재 여부를 원소 0의 셀이 정하고
있었습니다. JSON과 presence 비트맵이 같은 자리를 봅니다.
// JsonExporter — 접힌 배열
sf.Fields[0].IsRequired || row[sf.FirstField!.Index].HasValue ? … : null
// BinaryExporter — presence 비트맵
if (rows[at][column.TagCarrier.Index].HasValue) // TagCarrier == Cells[0]
그래서 Tag1이 없고 Tag2에 값이 있는 로우는 배열 전체가 null이 되고 그 값이
사라졌습니다. 원소 0이 비었다는 것은 원소 1~N에 대해 아무 말도 하지 않으므로, 이 대리는
그 자체로 틀린 것이었습니다. 위 표의 세 번째 줄이 그것을 고정합니다.
타입은 첫 컬럼이 계속 대표합니다. 배열 옵셔널리티의 규칙은 그대로입니다 — 바뀌는 것은 그 표시가 무엇을 정하는가이고, 어느 컬럼이 정하는가가 아닙니다.