[英]Why does a zero-length stackalloc make the C# compiler happy to allow conditional stackallocs?
下面的“修復”讓我很困惑; 這里的場景是根據大小有條件地決定是使用堆棧還是租用緩沖區 - 這是一個非常利基但有時是必要的優化,但是:使用“明顯”的實現(第 3 號,推遲明確的分配,直到我們真正想要分配它),編譯器向 CS8353 抱怨:
'Span<int>' 類型的 stackalloc 表達式的結果不能在此上下文中使用,因為它可能暴露在包含方法之外
簡短的復制(完整的復制如下)是:
// take your pick of:
// Span<int> s = stackalloc[0]; // works
// Span<int> s = default; // fails
// Span<int> s; // fails
if (condition)
{ // CS8353 happens here
s = stackalloc int[size];
}
else
{
s = // some other expression
}
// use s here
我在這里唯一能想到的是,編譯器確實在標記stackalloc
是stackalloc
發生 stackalloc 的上下文,並且揮舞着一個標志說“我無法證明這是否會在以后安全方法”,但是通過在開始時使用stackalloc[0]
,我們將“危險”上下文 scope 推得更高,現在編譯器很高興它永遠不會逃脫“危險” scope(即它實際上從未離開方法,因為我們在頂級范圍內聲明)。 這種理解是否正確,就可以證明的內容而言,這只是編譯器的限制?
(對我來說)真正有趣的是= stackalloc[0]
基本上是無操作的,這意味着至少在編譯形式中,工作編號 1 = stackalloc[0]
與失敗的編號 2 = default
相同。
完整再現(也可在 SharpLab 上查看 IL )。
using System;
using System.Buffers;
public static class C
{
public static void StackAllocFun(int count)
{
// #1 this is legal, just initializes s as a default span
Span<int> s = stackalloc int[0];
// #2 this is illegal: error CS8353: A result of a stackalloc expression
// of type 'Span<int>' cannot be used in this context because it may
// be exposed outside of the containing method
// Span<int> s = default;
// #3 as is this (also illegal, identical error)
// Span<int> s;
int[] oversized = null;
try
{
if (count < 32)
{ // CS8353 happens at this stackalloc
s = stackalloc int[count];
}
else
{
oversized = ArrayPool<int>.Shared.Rent(count);
s = new Span<int>(oversized, 0, count);
}
Populate(s);
DoSomethingWith(s);
}
finally
{
if (oversized is not null)
{
ArrayPool<int>.Shared.Return(oversized);
}
}
}
private static void Populate(Span<int> s)
=> throw new NotImplementedException(); // whatever
private static void DoSomethingWith(ReadOnlySpan<int> s)
=> throw new NotImplementedException(); // whatever
// note: ShowNoOpX and ShowNoOpY compile identically just:
// ldloca.s 0, initobj Span<int>, ldloc.0
static void ShowNoOpX()
{
Span<int> s = stackalloc int[0];
DoSomethingWith(s);
}
static void ShowNoOpY()
{
Span<int> s = default;
DoSomethingWith(s);
}
}
Span<T> / ref
功能本質上是一系列規則,scope 給定值可以通過值或引用轉義。 雖然這是根據方法范圍編寫的,但將其簡化為以下兩個語句之一會很有幫助:
跨度安全文檔詳細介紹了如何針對各種語句和表達式計算 scope。 不過,這里的相關部分是關於如何處理本地人的。
主要的收獲是,本地是否可以返回是在本地聲明時計算的。 在聲明局部變量時,編譯器檢查初始化器並決定局部變量是否可以從方法返回。 如果有初始化程序,那么如果能夠返回初始化表達式,則本地將能夠返回。
您如何處理聲明了本地但沒有初始化程序的情況? 編譯器必須做出決定:它可以返回還是不能返回? 在設計該功能時,我們決定默認為“它可以返回”,因為這是對現有模式造成最小摩擦的決定。
這確實給我們留下了一個問題,即開發人員如何聲明一個不能安全返回但又缺少初始化程序的本地。 最終我們確定了= stackalloc [0]
的模式。 這是一個可以安全優化的表達式,也是一個強有力的指標,基本上是一個要求,即本地返回是不安全的。
知道這解釋了您所看到的行為:
Span<int> s = stackalloc[0]
:返回是不安全的,因此后面的stackalloc
成功Span<int> s = default
:這是可以安全返回的,因為default
可以安全返回。 這意味着后面的stackalloc
失敗,因為您正在分配一個不安全的值以返回到標記為可安全返回的本地Span<int> s;
:這是安全的返回,因為這是未初始化的本地人的默認值。 這意味着后面的stackalloc
失敗,因為您正在分配一個不安全的值以返回到標記為可安全返回的本地 = stackalloc[0]
方法的真正缺點是它僅適用於Span<T>
。 這不是ref struct
的通用解決方案。 在實踐中,盡管對於其他類型來說這不是什么大問題。 有一些關於我們如何使它更普遍的猜測,但沒有足夠的證據證明在這一點上這樣做是合理的。
不是“為什么”的答案; 但是,您可以將其更改為三元運算符,將數組分配的結果切片到 Span:
public static void StackAllocFun(int count)
{
int[] oversized = null;
try
{
Span<int> s = ((uint)count < 32) ?
stackalloc int[count] :
(oversized = ArrayPool<int>.Shared.Rent(count)).AsSpan(0, count);
Populate(s);
DoSomethingWith(s);
}
finally
{
if (oversized is not null)
{
ArrayPool<int>.Shared.Return(oversized);
}
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.