內嵌陣列
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在相關 語言設計會議(LDM)的會議記錄中擷取的。
冠軍議題:https://github.com/dotnet/csharplang/issues/7431
總結
提供使用 InlineArrayAttribute 功能之結構類型的一般用途和安全機制。 提供一種用於在 C# 類別、結構和介面中宣告內嵌陣列的通用且安全的機制。
注意:舊版的此規格使用「ref-safe-to-escape」和「safe-to-escape」這些術語,這些術語是在 Span safety 功能規格中引入的。 ECMA 標準委員會 分別將名稱變更為 “ref-safe-context” 和 “safe-context”。 已優化安全上下文的值,以一致地使用「declaration-block」、「function-member」和「caller-context」。 這些規範文檔對這些術語使用了不同的措辭,也將「安全傳回」用作「呼叫者上下文」的同義詞。 此規格已更新為使用 C# 7.3 標準中的詞彙。
動機
此提案計劃解決 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers的許多限制。 具體來說,它的目標是允許:
- 使用 InlineArrayAttribute 功能存取結構類型的元素;
- 在
struct
、class
或interface
中宣告受控和非受控型別的內聯陣列。
並提供語言安全性驗證。
詳細設計
最近執行時新增了InlineArrayAttribute 功能。 簡言之,使用者可以宣告結構類型,如下所示:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
private object _element0;
}
執行時為 Buffer
類型提供特殊類型佈局:
- 類型大小被延展以容納來自 InlineArray 屬性的 10 個
object
類型的元素(在此範例中,類型來自結構中唯一的實例欄位_element0
)。 - 第一個元素會與實例欄位和結構的起始位置對齊
- 元素會依序在記憶體中配置,就像是陣列的元素一樣。
執行時會對結構中所有元素提供常規 GC 追蹤。
此提案會將這類類型稱為「內嵌陣列類型」。
內嵌數位類型的元素可以透過指標或 System.Runtime.InteropServices.MemoryMarshal.CreateSpan 傳回的跨實例來存取,/System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan API。 不過,指標方法和 API 都未能自動提供型別及界限檢查。
語言將提供類型安全和參考安全的方式來存取內嵌陣列類型的元素。 存取權會以範圍為基礎。 這限制對元素型態可用作類型參數的內嵌數組型態的支援。 例如,指標類型不能當做項目類型使用。 範圍類型的其他範例。
取得內嵌陣列型態的跨度類型實例
由於保證內聯陣列類型中的第一個元素在類型開頭對齊(沒有間距),編譯器會使用下列程式碼來取得 Span
值:
MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size);
而下列程式代碼可取得 ReadOnlySpan
值:
MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size);
若要減少使用端的 IL 大小,編譯器應能將兩個通用的可重複使用輔助程式加入至私有實作細節類型,並在同一程式中的所有使用端進行使用。
public static System.Span<TElement> InlineArrayAsSpan<TBuffer, TElement>(ref TBuffer buffer, int size) where TBuffer : struct
{
return MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size);
}
public static System.ReadOnlySpan<TElement> InlineArrayAsReadOnlySpan<TBuffer, TElement>(in TBuffer buffer, int size) where TBuffer : struct
{
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size);
}
元素存取
元素存取 將會擴充以支援內嵌陣列元素的存取。
element_access 包含 primary_no_array_creation_expression,接著是“[
”標記,然後是 argument_list,最後是“]
”標記。
argument_list 包含一或多個 自變數,並以逗號分隔。
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
element_access 的 argument_list 不允許包含 ref
或 out
參數。
如果下列條件至少有一項成立,element_access 會動態系結(§11.3.3):
-
primary_no_array_creation_expression 具有編譯時間類型
dynamic
。 -
argument_list 中至少有一個運算式的編譯時間類型為
dynamic
,而且 primary_no_array_creation_expression 沒有陣列類型,和 primary_no_array_creation_expression 沒有內嵌陣列型態,或參數清單中有多個項目。
在此情況下,編譯程式會將 element_access 分類為 類型 dynamic
的值。 在運行時,接著會使用運行時間類型,而不是 primary_no_array_creation_expression 和 argument_list 這些具有編譯時間類型 dynamic
的運算式的編譯時間類型,套用下列規則來判斷 element_access 的意義。 如果 primary_no_array_creation_expression 沒有編譯時間類型 dynamic
,則元素存取會經歷有限的編譯時間檢查,如 .11.6.5中所述。
如果 element_access 的 primary_no_array_creation_expression 是 array_type的值,則 element_access 是陣列存取(§12.8.12.2)。 如果 element_access 的 primary_no_array_creation_expression 是內嵌數位型態的變數或值,且 argument_list 是由單一自變數所組成,則 element_access 是內嵌數位元素存取。 否則,primary_no_array_creation_expression 應該是具有一或多個索引器成員之類別、結構或介面類型的變數或值,在此情況下,element_access 是索引器存取(§12.8.12.3)。
內嵌陣列元素存取
對於內嵌陣列元素存取,element_access 的 primary_no_array_creation_expression 必須是內嵌陣列類型的變數或值。 此外,內嵌陣列元素存取的 argument_list 不允許包含具名參數。 argument_list 必須包含一個單一的運算式,而且該運算式必須是
- 類型為
int
或 - 隱含轉換成
int
、 或 - 隱含轉換成
System.Index
、 或 - 隱含轉換成
System.Range
。
當表達式類型為 int 時
如果 primary_no_array_creation_expression 是可寫入的變數,則評估內嵌數組元素訪問的結果為一個可寫入的變數,等同於在 primary_no_array_creation_expression上透過 System.Span<T> InlineArrayAsSpan
方法返回的 System.Span<T>
實例上使用該整數值來調用 public ref T this[int index] { get; }
。 為了進行 ref-safety 分析,ref-safe-context/存取的安全內容 相當於使用簽章 static ref T GetItem(ref InlineArrayType array)
叫用方法的相同。
僅當 primary_no_array_creation_expression 可移動時,生成的變數才會被視為可移動。
如果 primary_no_array_creation_expression 是只讀變數,則評估內嵌陣列元素存取的結果是一個相當於在 primary_no_array_creation_expression上,由 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
方法傳回之 System.ReadOnlySpan<T>
實例上,用該整數值叫用 public ref readonly T this[int index] { get; }
的只讀變數。 為了進行 ref-safety 分析,ref-safe-context/存取的安全內容 相當於使用簽章 static ref readonly T GetItem(in InlineArrayType array)
叫用方法的相同。
當且僅當 primary_no_array_creation_expression 是可移動的時候,所產生的變數才會被視為可移動。
如果 primary_no_array_creation_expression 是一個值,那麼內嵌陣列元素存取的評估結果等同於在 primary_no_array_creation_expression上,利用 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
方法所返回的 System.ReadOnlySpan<T>
實例,使用該整數值來調用 public ref readonly T this[int index] { get; }
的結果。 為了進行 ref-safety 分析,ref-safe-context/的存取安全內容 與具備簽章 static T GetItem(InlineArrayType array)
的方法調用中的情況相當。
例如:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
}
void M1(Buffer10<int> x)
{
ref int a = ref x[0]; // Ok, equivalent to `ref int a = ref InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10)[0]`
}
void M2(in Buffer10<int> x)
{
ref readonly int a = ref x[0]; // Ok, equivalent to `ref readonly int a = ref InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)[0]`
ref int b = ref x[0]; // An error, `x` is a readonly variable => `x[0]` is a readonly variable
}
Buffer10<int> GetBuffer() => default;
void M3()
{
int a = GetBuffer()[0]; // Ok, equivalent to `int a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(GetBuffer(), 10)[0]`
ref readonly int b = ref GetBuffer()[0]; // An error, `GetBuffer()[0]` is a value
ref int c = ref GetBuffer()[0]; // An error, `GetBuffer()[0]` is a value
}
在宣告的內嵌陣列界限之外使用常數表達式對內嵌陣列進行索引是編譯時錯誤。
當表達式可以隱式轉換為 int
表達式會被轉換成為 int,然後元素存取會按照 當表達式類型為 int 的情況 區段中所述進行解譯。
當語句可以被隱含轉換成 System.Index
表達式會轉換成 System.Index
,然後轉換成以 int 為基礎的索引值,如 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support所述,假設集合的長度在編譯時期已知,且等於 primary_no_array_creation_expression內嵌數位型態中的元素數量。 然後,元素存取會解譯為 當表達式類型為 int 區段中所述。
當表達式可以隱式轉換成 System.Range
時
如果 primary_no_array_creation_expression 是可寫的變數,則評估內嵌陣列元素存取的結果等同於在 primary_no_array_creation_expression上呼叫 System.Span<T> InlineArrayAsSpan
方法所返回的 System.Span<T>
實例上的 public Span<T> Slice (int start, int length)
值。
為了進行 ref-safety 分析,ref-safe-context/存取的安全內容 相當於使用簽章 static System.Span<T> GetSlice(ref InlineArrayType array)
叫用方法的相同。
如果 primary_no_array_creation_expression 是只讀變數,評估內嵌陣列元素存取的結果就是相當於在 primary_no_array_creation_expression上 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
方法所傳回 System.ReadOnlySpan<T>
之實例上叫用 public ReadOnlySpan<T> Slice (int start, int length)
的值。
為了進行 ref-safety 分析,ref-safe-context/存取的安全內容 相當於使用簽章 static System.ReadOnlySpan<T> GetSlice(in InlineArrayType array)
叫用方法的相同。
如果 primary_no_array_creation_expression 為值,則會報告錯誤。
Slice
方法調用的參數是從被轉換為 System.Range
的索引表達式計算而來,如 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support所述。假設集合的長度在編譯時期已知,且等於 primary_no_array_creation_expression中內嵌數組類型的元素數量。
如果編譯時期已知 start
為 0,且 length
小於或等於內嵌陣列類型中的元素的數量,編譯程式可以省略對 Slice
的呼叫。 編譯程式也可以在編譯時期得知切割超出內嵌數位界限時回報錯誤。
例如:
void M1(Buffer10<int> x)
{
System.Span<int> a = x[..]; // Ok, equivalent to `System.Span<int> a = InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10).Slice(0, 10)`
}
void M2(in Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x[..]; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10).Slice(0, 10)`
System.Span<int> b = x[..]; // An error, System.ReadOnlySpan<int> cannot be converted to System.Span<int>
}
Buffer10<int> GetBuffer() => default;
void M3()
{
_ = GetBuffer()[..]; // An error, `GetBuffer()` is a value
}
轉換
將會新增一個從表達式來的內嵌陣列轉換。 內嵌陣列轉換是一種 標準轉換。
從內嵌陣列型態的運算式隱含轉換成以下類型:
System.Span<T>
System.ReadOnlySpan<T>
不過,將只讀變數轉換成 System.Span<T>
或將值轉換成任一類型是錯誤。
例如:
void M1(Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)`
System.Span<int> b = x; // Ok, equivalent to `System.Span<int> b = InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10)`
}
void M2(in Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)`
System.Span<int> b = x; // An error, readonly mismatch
}
Buffer10<int> GetBuffer() => default;
void M3()
{
System.ReadOnlySpan<int> a = GetBuffer(); // An error, ref-safety
System.Span<int> b = GetBuffer(); // An error, ref-safety
}
基於 ref-safety 分析的目的,轉換時的 安全內容 等同於調用簽章為 static System.Span<T> Convert(ref InlineArrayType array)
或 static System.ReadOnlySpan<T> Convert(in InlineArrayType array)
方法的 安全內容。
清單樣式
明確指派檢查
一般明確的指派規則適用於具有內嵌陣列型態的變數。
集合字面值
內嵌陣列類型的實例在 spread_element中是有效的表達式。
下列功能未隨附於 C# 12 中。 它仍然是一個公開的建議。 此範例中的程式代碼會產生 CS9174:
內聯陣列類型是 集合表示式的有效目標類型,適用於 可建構集合。 例如:
Buffer10<int> b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // initializes user-defined inline array
集合字面值的長度必須與目標內嵌陣列類型的長度相符。 如果常值長度在編譯時期已知且不符合目標長度,則會報告錯誤。 否則,一旦遇到不相符的情況,就會在執行時期拋出例外狀況。 確切的例外狀況類型為 TBD。 某些候選項包括:System.NotSupportedException、System.InvalidOperationException。
InlineArrayAttribute 應用程式的驗證
編譯程式會驗證 InlineArrayAttribute 應用程式的下列層面:
- 目標類型是 非記錄 結構
- 目標類型只有一個字段
- 指定的長度 > 0
- 目標結構未指定明確的版面配置
物件初始化表達式中的內嵌陣列元素
根據預設,元素初始化將不會透過格式 '[' argument_list ']'
的 initializer_target 來支援(請參閱 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128173-object-initializers):
static C M2() => new C() { F = {[0] = 111} }; // error CS1913: Member '[0]' cannot be initialized. It is not a field or property.
class C
{
public Buffer10<int> F;
}
不過,如果內嵌陣列類型明確定義適當的索引器,物件初始化器會使用它:
static C M2() => new C() { F = {[0] = 111} }; // Ok, indexer is invoked
class C
{
public Buffer10<int> F;
}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
public T this[int i]
{
get => this[i];
set => this[i] = value;
}
}
foreach 語句
foreach 語句 將會調整,以允許使用內嵌陣列類型作為 foreach 語句中的集合。
例如:
foreach (var a in getBufferAsValue())
{
WriteLine(a);
}
foreach (var b in getBufferAsWritableVariable())
{
WriteLine(b);
}
foreach (var c in getBufferAsReadonlyVariable())
{
WriteLine(c);
}
Buffer10<int> getBufferAsValue() => default;
ref Buffer10<int> getBufferAsWritableVariable() => default;
ref readonly Buffer10<int> getBufferAsReadonlyVariable() => default;
相當於:
Buffer10<int> temp = getBufferAsValue();
foreach (var a in (System.ReadOnlySpan<int>)temp)
{
WriteLine(a);
}
foreach (var b in (System.Span<int>)getBufferAsWritableVariable())
{
WriteLine(b);
}
foreach (var c in (System.ReadOnlySpan<int>)getBufferAsReadonlyVariable())
{
WriteLine(c);
}
我們將支援內嵌陣列的 foreach
,即使因為跨類型對轉譯的參與使其在 async
方法中受到限制也一樣。
設計開放性問題
替代方案
插入陣列類型語法
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general 的文法將會依下列方式進行調整:
array_type
: non_array_type rank_specifier+
;
rank_specifier
: '[' ','* ']'
+ | '[' constant_expression ']'
;
constant_expression 的類型必須可以隱含地轉換為類型 int
,而且值必須是非零正整數。
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#1721-general 區段的相關部分將依照下列方式進行調整。
陣列類型的文法產生式提供在 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general。
陣列類型表示為 non_array_type,後接一或多個 rank_specifier。
non_array_type 是任何 類型,但不是 array_type。
陣列類型的等級由 array_type中最左邊的 rank_specifier 所指定:rank_specifier 表示陣列是一個等級為一,再加上 rank_specifier中“,
”標記的數量的陣列。
陣列類型的元素類型是刪除最左邊 rank_specifier所產生的類型:
- 表單
T[ constant_expression ]
的陣列類型是匿名內嵌陣列類型,其長度由 constant_expression 和非陣列元素類型T
表示。 - 一種形式為
T[ constant_expression ][R₁]...[Rₓ]
的陣列類型是匿名內嵌陣列類型,其長度由 constant_expression 和元素類型T[R₁]...[Rₓ]
表示。 - 格式如
T[R]
(其中 R 不是 constant_expression)的陣列類型,是具有維度R
且元素類型為非陣列類型T
的一般陣列類型。 - 形如
T[R][R₁]...[Rₓ]
的陣列類型(其中 R 不是 constant_expression)是一種一般的陣列類型,具有順位R
和元素類型T[R₁]...[Rₓ]
。
實際上,rank_specifier會先從左至右讀取 ,再 最終的非陣列項目類型。
範例:
int[][,,][,]
中的類型是由三維陣列組成的單維陣列,這些三維陣列又由int
二維陣列組成。 結束範例
在運行時,陣列類型的數值可以是 null
或該陣列類型實例的參考。
附注:遵循 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#176-array-covariance的規則,該值也可能是協變陣列類型的參考。 尾注
匿名內嵌陣列類型是編譯程式合成的內嵌陣列類型,具有內部可存取性。 項目類型必須是類型,才能當做類型自變數使用。 不同於明確宣告的內嵌數位類型,匿名內嵌數位類型無法依名稱參考,只能由 array_type 語法來參考。 在相同程式的內容中,任何兩個 array_type表示相同元素類型和相同長度的內聯陣列類型,都會參照相同的匿名內聯陣列類型。
除了內部存取性之外,編譯器還會使用必要的自定義修飾符(確切類型待定),該修飾符應用於簽名中的匿名內嵌陣列類型參考,以防止跨組件邊界消耗使用匿名內嵌陣列類型的 API。
陣列建立表達式
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
假設目前的文法,使用 constant_expression 取代 expression_list 已經具有配置指定長度之一般單維數位型態的意義。 因此,array_creation_expression 會繼續代表一般陣列的分配。
不過,rank_specifier 的新形式可用來將匿名內聯陣列類型併入已配置陣列的元素類型。
例如,下列表達式會建立長度為 2 的一般數位,其元素類型為匿名內嵌數位類型,且元素類型為 int,長度為 5:
new int[2][5];
new int[][5] {default, default};
new [] {default(int[5]), default(int[5])};
陣列初始化器
數位初始化表達式未在 C# 12 中實作。 本節仍然是一個積極的建議。
將會調整 陣列初始化器 區段,以允許使用 array_initializer 初始化行內陣列類型(不需要變更文法)。
array_initializer
: '{' variable_initializer_list? '}'
| '{' variable_initializer_list ',' '}'
;
variable_initializer_list
: variable_initializer (',' variable_initializer)*
;
variable_initializer
: expression
| array_initializer
;
內嵌陣列的長度必須由目標類型明確指定。
例如:
int[5] a = {1, 2, 3, 4, 5}; // initializes anonymous inline array of length 5
Buffer10<int> b = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // initializes user-defined inline array
var c = new int[][] {{11, 12}, {21, 22}, {31, 32}}; // An error for the nested array initializer
var d = new int[][2] {{11, 12}, {21, 22}, {31, 32}}; // An error for the nested array initializer
詳細設計(選項 2)
請注意,基於此提案的目的,「固定大小緩衝區」一詞是指建議的「安全固定大小緩衝區」功能,而不是 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers中所述的緩衝區。
在此設計中,固定大小的緩衝區類型不會受到語言的一般特殊處理。 有一種特殊語法可用來宣告表示固定大小緩衝區的成員,以及有關使用這些成員的新規則。 這些領域從語言的角度來看並不是。
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#155-fields 中 variable_declarator 的文法將會延伸,以允許指定緩衝區的大小:
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
+ | fixed_size_buffer_declarator
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
fixed_size_buffer_declarator 引入具有特定元素類型的固定大小緩衝區。
緩衝區項目類型是 field_declaration
中指定的 類型。 固定大小的緩衝區聲明符引入新的成員,並透過一個標識符來命名此成員,後接被 [
和 ]
標記包圍的常數表達式。 常數表達式表示該固定大小緩衝區宣告子所引進之成員中的元素數目。 常數表達式的類型必須隱含轉換成類型 int
,而且值必須是非零正整數。
固定大小的緩衝區元素應該循序配置在記憶體中,就像它們是陣列的元素一樣。
介面中包含 fixed_size_buffer_declarator 的 field_declaration 必須有 static
修飾詞。
視情況而定(詳細數據如下所述),固定大小的緩衝區成員的存取權會分類為 System.ReadOnlySpan<S>
或 System.Span<S>
的值(絕不是變數),其中 S 是固定大小緩衝區的元素類型。 這兩種類型都提供索引器,返回特定元素的參考,並具有適當的只讀屬性,這可以防止在語言規則不允許時直接對元素進行賦值。
這會將可作為固定大小緩衝區元素類型的集合限制為可用作類型引數的類型。 例如,指標類型不能當做項目類型使用。
產生的範圍實例長度會等於固定大小緩衝區上宣告的大小。 在宣告的固定大小緩衝區界限之外,使用常數表達式編製範圍索引是編譯時間錯誤。
值的 安全內容 會等於容器 安全內容,就像是存取備份數據做為字段一樣。
表達式中的固定大小緩衝區
固定大小緩衝區成員的查詢過程與欄位的查詢過程完全相同。
您可以使用 simple_name 或 member_access,在表示式中參考固定大小的緩衝區。
當實例中的固定大小緩衝區成員被簡單名稱所參考時,其效果與形式為 this.I
的成員存取相同,其中 I
是固定大小的緩衝區成員。 當靜態固定大小緩衝區成員參考為簡單名稱時,效果會與窗體 E.I
的成員存取相同,其中 I
是固定大小的緩衝區成員,而 E
是宣告類型。
非只讀固定大小緩衝區
在以 E.I
為形式的成員存取中,如果 E
是結構類型,且在該結構類型中查找 I
的成員識別出一個非唯讀的固定大小的實例成員,則評估 E.I
,並分類如下:
- 如果
E
分類為值,則E.I
只能作為具有System.Index
型別索引的 元素存取 的 primary_no_array_creation_expression,或者類型隱性轉換為 int。元素存取的結果是位於指定位置的固定大小成員元素,分類為值。 - 否則,如果
E
分類為唯讀變數,而表達式的結果會分類為類型System.ReadOnlySpan<S>
的值,其中 S 是I
的項目類型。 值可用來存取成員的元素。 - 否則,
E
會分類為可寫入的變數,而表達式的結果會分類為類型System.Span<S>
的值,其中 S 是I
的元素類型。 值可用來存取成員的元素。
在表單 E.I
的成員存取中,如果 E
屬於類別類型,且在該類別類型中,I
的成員查閱識別出非唯讀固定大小的實例成員,則會對 E.I
進行評估,並將其分類為類型 System.Span<S>
的值,其中 S 是 I
的元素類型。
在窗體的成員存取 E.I
中,如果成員查閱 I
識別非只讀靜態固定大小成員,則會評估 E.I
並分類為類型 System.Span<S>
的值,其中 S 是 I
的元素類型。
唯讀固定大小的緩衝區
當 field_declaration 包含 readonly
修飾詞時,fixed_size_buffer_declarator 所引進的成員是只讀固定大小緩衝區。
只能在相同類型的實例建構函式、init 成員或靜態建構函式中,直接指派只讀固定大小緩衝區的元素。
具體來說,只有在以下情境下,才允許直接指派至只讀固定大小緩衝區的元素:
- 針對實例成員,在包含成員宣告之型別的實例建構函式或 init 成員中;針對靜態成員,在包含成員宣告之型別的靜態建構函式中。 在這些也是唯一有效的情境中,可以將只讀固定大小緩衝區的元素作為
out
或ref
參數傳遞。
嘗試指派給只讀固定大小的緩衝區的元素,或在任何其他上下文中將其作為 out
或 ref
參數傳遞時,都是編譯時錯誤。
這可透過下列方式達成。
會評估只讀固定大小緩衝區的成員存取權,並分類如下:
- 在窗體
E.I
的成員存取中,如果E
屬於結構類型,且E
分類為值,則E.I
只能當做 專案存取 的 primary_no_array_creation_expression,且索引為System.Index
型別,或隱含轉換成 int 的類型。專案存取的結果是位於指定位置的固定大小成員專案,分類為值。 - 如果存取發生在允許直接指派到只讀固定大小緩衝區元素的上下文中,則該表達式的結果會被分類為類型為
System.Span<S>
的值,其中 S 是固定大小緩衝區的元素類型。 值可用來存取成員的元素。 - 否則,表達式會分類為類型為
System.ReadOnlySpan<S>
的值,其中 S 是固定大小緩衝區的元素類型。 值可用來存取成員的元素。
確定性指派檢查
固定大小緩衝區不會受限於明確的指派檢查,而且會忽略固定大小的緩衝區成員,以便進行結構類型變數的明確指派檢查。
當固定大小緩衝區成員是靜態的,或是包含固定大小緩衝區成員之結構變數的最外層是靜態變數、類別實例的實例變數或數位元素時,固定大小的緩衝區元素會自動初始化為其預設值。 在其他所有情況下,固定大小緩衝區的初始內容是未定義的。
元數據
元數據發出和產生程序代碼
編譯程式在元數據編碼時將依賴最近新增的 System.Runtime.CompilerServices.InlineArrayAttribute
。
固定大小的緩衝區,例如下列偽程式碼:
// Not valid C#
public partial class C
{
public int buffer1[10];
public readonly int buffer2[10];
}
將會發出為特別裝飾結構類型的欄位。
對等的 C# 程式代碼會是:
public partial class C
{
public Buffer10<int> buffer1;
public readonly Buffer10<int> buffer2;
}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
[UnscopedRef]
public System.Span<T> AsSpan()
{
return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref _element0, 10);
}
[UnscopedRef]
public readonly System.ReadOnlySpan<T> AsReadOnlySpan()
{
return System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan(
ref System.Runtime.CompilerServices.Unsafe.AsRef(in _element0), 10);
}
}
類型及其成員的實際命名慣例將待定。 架構可能會包含一組預先定義的「緩衝區」類型,其中涵蓋一組有限的緩衝區大小。 當預先定義的類型不存在時,編譯程式會在所建置的模組中合成它。 產生的類型名稱將會「可讀」,以支援其他語言的取用。
為存取產生的程式代碼,例如:
public partial class C
{
void M1(int val)
{
buffer1[1] = val;
}
int M2()
{
return buffer2[1];
}
}
相當於:
public partial class C
{
void M1(int val)
{
buffer.AsSpan()[1] = val;
}
int M2()
{
return buffer2.AsReadOnlySpan()[1];
}
}
元數據匯入
當編譯器匯入類型宣告 T 時,且符合以下所有條件時:
-
T 是以
InlineArray
屬性裝飾的結構類型。 - 在 T 內宣告的第一個實例欄位具有 F類型,
-
T內有
public System.Span<F> AsSpan()
, - 在 T內有
public readonly System.ReadOnlySpan<T> AsReadOnlySpan()
或public System.ReadOnlySpan<T> AsReadOnlySpan()
。
欄位會被視為 C# 固定大小緩衝區,且元素類型 F。否則,欄位會被視為類型為 T的一般欄位。
方法或屬性群組,例如語言中的方法
其中一個想法是將這些成員視為更像是方法群組,也就是說,它們本身不會自動成為值,但必要時可以轉換成一個。 以下是運作方式:
- 安全的固定大小緩衝區存取有自己的分類(例如方法群組和 Lambda)
- 它們可以直接作為語言操作進行索引(而不是通過 span 類型)來產生變數(如果緩衝區處於只讀狀態,則該變數為唯讀,與結構中的字段相同)
- 它們具有從表達式隱式轉換成
Span<T>
和ReadOnlySpan<T>
,但如果它們位於唯讀上下文中,使用前者將是錯誤的。 - 其自然類型是
ReadOnlySpan<T>
,因此,如果他們參與類型推斷(例如 var、最佳公共同類型或泛型),它們將貢獻該類型。
C/C++固定大小緩衝區
C/C++有不同的固定大小緩衝區概念。 例如,有一種「零長度固定大小緩衝區」的概念,通常用來指出數據為「可變長度」。 這項提案的目標並非達到與其互通。
LDM 會議
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-03.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-10.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-01.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#inline-arrays
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-17.md#inline-arrays
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-17.md#inline-arrays-as-record-structs