Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Annotazioni
Questo articolo è una specifica delle funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.
Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono riportate nelle note pertinenti della riunione di progettazione linguistica (LDM) .
Ulteriori dettagli sul processo di adozione delle specifiche di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche .
Questione prioritaria: https://github.com/dotnet/csharplang/issues/8714
Sommario
Introduciamo il supporto di prima classe per Span<T>
e ReadOnlySpan<T>
nel linguaggio, inclusi i nuovi tipi di conversione implicita e li consideriamo in più posizioni, consentendo una programmazione più naturale con questi tipi integrali.
Motivazione
Dall'introduzione in C# 7.2, Span<T>
e ReadOnlySpan<T>
hanno funzionato nel linguaggio e nella libreria di classi di base in molti modi chiave. Questo è ideale per gli sviluppatori, poiché l'introduzione migliora le prestazioni senza costi per la sicurezza degli sviluppatori. Tuttavia, il linguaggio ha mantenuto questi tipi a distanza in diversi modi chiave, il che rende difficile esprimere l'intento delle API e porta a una notevole duplicazione dell'ambito per le nuove API. Ad esempio, il BCL ha aggiunto una serie di nuove tensor primitive APIs in .NET 9, ma queste API sono tutte offerte su ReadOnlySpan<T>
. C# non riconosce la relazione tra ReadOnlySpan<T>
, Span<T>
e T[]
, quindi anche se sono presenti conversioni definite dall'utente tra questi tipi, non possono essere usate per i ricevitori di metodi di estensione, non possono comporre con altre conversioni definite dall'utente e non supportano tutti gli scenari di inferenza dei tipi generici.
Gli utenti devono usare conversioni esplicite o argomenti di tipo, il che significa che gli strumenti dell'IDE non guidano gli utenti a usare queste API, perché nulla indicherà all'IDE che è valido passare questi tipi dopo la conversione. Per garantire la massima usabilità per questo stile di API, il BCL dovrà definire un intero set di overload Span<T>
e T[]
, che costituirebbe una notevole quantità di area duplicata da mantenere senza alcun beneficio reale. Questa proposta cerca di risolvere il problema facendo in modo che la lingua riconosca più direttamente questi tipi e conversioni.
Ad esempio, la BCL può aggiungere solo un sovraccarico di qualsiasi aiuto simile a MemoryExtensions
.
int[] arr = [1, 2, 3];
Console.WriteLine(
arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
);
public static class MemoryExtensions
{
public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}
In precedenza, erano necessari gli overload di Span e array per rendere utilizzabile il metodo di estensione su variabili di tipo Span/array, poiché le conversioni definite dall'utente (che esistono tra Span/array/ReadOnlySpan) non vengono considerate per i ricevitori di estensione.
Progettazione dettagliata
Le modifiche apportate a questa proposta saranno legate a LangVersion >= 14
.
Conversioni di intervallo
Viene aggiunto un nuovo tipo di conversione implicita all'elenco in §10.2.1, una conversione di intervallo implicito . La conversione è una conversione di tipo e viene definita come segue:
Una conversione implicita dell'intervallo consente di convertire array_types
, System.Span<T>
, System.ReadOnlySpan<T>
e string
tra loro come indicato di seguito:
- Da qualsiasi
array_type
unidimensionale con tipo elementoEi
aSystem.Span<Ei>
- Da qualsiasi
array_type
unidimensionale con tipo di elementoEi
aSystem.ReadOnlySpan<Ui>
, a condizione cheEi
sia convertibile con covarianza (§18.2.3.3) aUi
- Da
System.Span<Ti>
aSystem.ReadOnlySpan<Ui>
, a condizione cheTi
sia covarianza convertibile (§18.2.3.3) aUi
- Da
System.ReadOnlySpan<Ti>
aSystem.ReadOnlySpan<Ui>
, a condizione cheTi
sia covarianza convertibile (§18.2.3.3) aUi
- Da
string
aSystem.ReadOnlySpan<char>
Qualsiasi tipo Span/ReadOnlySpan viene considerato applicabile per la conversione se sono ref struct
e corrispondono al nome completo qualificato (LDM 2024-06-24).
Aggiungiamo anche la conversione implicita dell'intervallo all'elenco delle conversioni implicite standard (§10.4.2). In questo modo si consente alla risoluzione dell'overload di considerare questi elementi durante l'esecuzione della risoluzione degli argomenti, come nella proposta dell'API menzionata in precedenza.
Le conversioni esplicite degli intervalli sono le seguenti:
- Tutte le conversioni implicite di span.
- Da un array_type con tipo di elemento
Ti
aSystem.Span<Ui>
oSystem.ReadOnlySpan<Ui>
, purché esista una conversione di riferimento esplicita daTi
aUi
.
Non esiste una conversione esplicita standard di intervalli, a differenza di altre conversioni esplicite standard () (§10.4.3) che esistono sempre, data la conversione implicita standard opposta.
Conversioni definite dall'utente
Le conversioni definite dall'utente non vengono considerate durante la conversione tra tipi per i quali esiste una conversione di intervallo implicita o esplicita.
Le conversioni di intervalli impliciti sono escluse dalla regola che non è possibile definire un operatore definito dall'utente tra i tipi per i quali esiste una conversione non definita dall'utente (§10.5.2 Conversioni definite dall'utente consentite). Ciò è necessario in modo che la BCL possa continuare a definire gli operatori di conversione Span esistenti anche quando passano a C# 14 (sono ancora necessari per versioni del linguaggio inferiori e anche perché questi operatori vengono usati nella generazione del codice delle nuove conversioni di intervalli standard). Ma può essere considerato come un dettaglio di implementazione (codegen e lower LangVersions non fanno parte della specifica) e Roslyn viola comunque questa parte della specifica (questa particolare regola sulle conversioni definite dall'utente non viene applicata).
Ricevitore di estensioni
Proponiamo di aggiungere anche la conversione implicita di span alla lista delle conversioni implicite accettabili per il primo parametro di un metodo di estensione quando si determina la sua applicabilità (12.8.9.3) (modifica in grassetto):
Un metodo di estensione
Cᵢ.Mₑ
è idoneo se:
Cᵢ
è una classe non generica e non annidata- Il nome di
Mₑ
è identifierMₑ
è accessibile e applicabile quando applicato agli argomenti come metodo statico, come illustrato in precedenza- Esiste una conversione implicita di identità, di riferimento
o di boxing, boxing o span da expr al tipo del primo parametro diMₑ
. La conversione di Span non viene considerata quando la risoluzione di sovraccarico viene eseguita per una conversione del gruppo di metodi.
Si noti che la conversione implicita di span non viene considerata per il ricevitore dell'estensione nelle conversioni di gruppi di metodi (LDM 2024-07-15) che consente al codice seguente di continuare a funzionare senza generare un errore in fase di compilazione CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates
:
using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
public static void M<T>(this Span<T> s, T x) => Console.Write(1);
public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}
Come possibile lavoro futuro, potremmo considerare la rimozione di questa condizione per cui la conversione span non sia considerata per il ricevitore di estensioni nelle conversioni del gruppo di metodi, e invece implementare modifiche affinché uno scenario simile a quello sopra menzionato finisca per chiamare correttamente l'overload Span
.
- Il compilatore potrebbe emettere un thunk che prende l'array come ricevitore ed esegue la conversione dello span al suo interno (in modo analogo all'utente che crea manualmente il delegato come
x => new int[0].M(x)
). - I delegati di valore, se fossero implementati, potrebbero accettare direttamente il
Span
come ricevitore.
Varianza
Lo scopo della sezione della varianza nella conversione dell'intervallo implicito è replicare una certa entità di covarianza per System.ReadOnlySpan<T>
. Le modifiche di runtime devono essere necessarie per implementare completamente la varianza tramite generics (vedere https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/ref-struct-interfaces.md per l'uso di tipi ref struct
in generics), ma è possibile consentire una quantità limitata di covarianza tramite l'uso di un'API .NET 9 proposta: https://github.com/dotnet/runtime/issues/96952. Ciò consentirà al linguaggio di trattare System.ReadOnlySpan<T>
come se il T
fosse dichiarato come out T
in alcuni scenari. Tuttavia, non applichiamo questa conversione attraverso tutti gli scenari di varianza e non la aggiungiamo alla definizione di varianza-convertibile in §18.2.3.3. Se in futuro modificassimo il runtime per comprendere più a fondo la varianza, potremmo apportare una modifica secondaria di rottura per riconoscerla pienamente nel linguaggio di programmazione.
Modelli
Si noti che quando ref struct
sono usati come tipo in qualsiasi modello, sono consentite solo le conversioni di identità.
class C<T> where T : allows ref struct
{
void M1(T t) { if (t is T x) { } } // ok (T is T)
void M2(R r) { if (r is R x) { } } // ok (R is R)
void M3(T t) { if (t is R x) { } } // error (T is R)
void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }
Dalla specifica di l'operatore is-type (§12.12.12.1):
Il risultato dell'operazione
E is T
[...] è un valore booleano che indica seE
è diverso da null e può essere convertito correttamente in un tipoT
attraverso una conversione di riferimento, una conversione di incapsulamento (boxing), una conversione di disincapsulamento (unboxing), una conversione di wrapping o una conversione di unwrapping.[...]
Se
T
è un tipo di valore non annullabile, il risultato ètrue
seD
eT
sono dello stesso tipo.
Questo comportamento non cambia con questa funzionalità, quindi non sarà possibile scrivere modelli per Span
/ReadOnlySpan
, anche se sono possibili modelli simili per le matrici (inclusa la varianza):
using System;
M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints
void M1<T>(T t)
{
if (t is object[] r) Console.WriteLine(r[0]); // ok
}
void M2<T>(T t) where T : allows ref struct
{
if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}
Generazione di codice
Le conversioni saranno sempre presenti, indipendentemente dal fatto che siano presenti helper di runtime usati per implementarli (LDM 2024-05-13). Se gli helper non sono presenti, il tentativo di usare la conversione genererà un errore in fase di compilazione che manca un membro richiesto dal compilatore.
Il compilatore prevede di usare gli helper o gli equivalenti seguenti per implementare le conversioni:
Conversione | Aiutanti |
---|---|
da array a intervallo |
static implicit operator Span<T>(T[]) (definito in Span<T> ) |
conversione di array a ReadOnlySpan |
static implicit operator ReadOnlySpan<T>(T[]) (definito in ReadOnlySpan<T> ) |
Conversione da Span a ReadOnlySpan |
static implicit operator ReadOnlySpan<T>(Span<T>) (definito in Span<T> ) e static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
ReadOnlySpan in ReadOnlySpan | static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
da string a ReadOnlySpan | static ReadOnlySpan<char> MemoryExtensions.AsSpan(string) |
Si noti che MemoryExtensions.AsSpan
viene usato invece dell'operatore implicito equivalente definito in string
.
Ciò significa che il codegen è diverso tra LangVersions (l'operatore implicito viene usato in C# 13; il metodo statico AsSpan
viene usato in C# 14).
D'altra parte, la conversione può essere generata in .NET Framework (il metodo AsSpan
esiste, mentre l'operatore string
non esiste).
La conversione esplicita da array a (ReadOnly)Span prima converte esplicitamente dall'array di origine a un array con il tipo di elemento di destinazione e poi in (ReadOnly)Span utilizzando lo stesso helper che una conversione implicita userebbe, ossia il op_Implicit(T[])
corrispondente.
Miglior conversione dell'espressione
Conversione migliore dall'espressione (§12.6.4.5) viene aggiornata per preferire conversioni implicite degli intervalli. Le modifiche alla risoluzione dell'overload delle espressioni di raccolta si basano su.
Dato un
C₁
di conversione implicita che esegue la conversione da un'espressioneE
a un tipoT₁
e una conversione implicitaC₂
che converte da un'espressioneE
a un tipoT₂
,C₁
è una conversione migliore rispetto aC₂
se si verifica una delle seguenti condizioni:
E
è un'espressione di raccolta , eC₁
è una conversione di raccolta di espressioni.E
non è un'espressione di raccolta e vale una delle seguenti condizioni:
E
corrisponde esattamenteT₁
eE
non corrisponde esattamenteT₂
E
non corrisponde esattamente né aT₁
né aT₂
, eC₁
è una conversione implicita dello span eC₂
non è una conversione implicita dello spanE
corrisponde esattamente sia aT₁
sia aT₂
, oppure a nessuna delle due, siaC₁
cheC₂
sono una conversione implicita dell'intervallo, eT₁
è un miglior obiettivo di conversione rispetto aT₂
.E
è un gruppo di metodi,T₁
è compatibile con il singolo metodo migliore del gruppo di metodi per la conversioneC₁
eT₂
non è compatibile con il singolo metodo migliore del gruppo di metodi per la conversioneC₂
Destinazione di conversione migliore
miglior target di conversione (§12.6.4.7) viene aggiornato in modo da preferire ReadOnlySpan<T>
rispetto a Span<T>
.
Dato due tipi
T₁
eT₂
,T₁
è una migliore destinazione di conversione rispetto aT₂
se si verifica una delle seguenti condizioni:
T₁
èSystem.ReadOnlySpan<E₁>
,T₂
èSystem.Span<E₂>
e esiste una conversione di identità daE₁
aE₂
T₁
èSystem.ReadOnlySpan<E₁>
,T₂
èSystem.ReadOnlySpan<E₂>
e esiste una conversione implicita daT₁
aT₂
e non esiste alcuna conversione implicita daT₂
aT₁
esiste- Almeno uno di
T₁
oT₂
non èSystem.ReadOnlySpan<Eᵢ>
e non èSystem.Span<Eᵢ>
e una conversione implicita daT₁
aT₂
esiste e non esiste alcuna conversione implicita daT₂
aT₁
- ...
Progettare riunioni:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#preferring-readonlyspant-over-spant-conversions
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-09.md#first-class-span-open-questions
Osservazioni di migliorezza
Il criterio della migliore conversione da espressione dovrebbe garantire che, ogni volta che un sovraccarico diventa applicabile a causa delle nuove conversioni di intervallo, venga evitata qualsiasi ambiguità potenziale con un altro sovraccarico perché viene preferito il sovraccarico appena applicabile.
Senza questa regola, il codice seguente compilato correttamente in C# 13 genera un errore di ambiguità in C# 14 a causa della nuova conversione implicita standard dalla matrice a ReadOnlySpan applicabile a un ricevitore di metodi di estensione:
using System;
using System.Collections.Generic;
var a = new int[] { 1, 2, 3 };
a.M();
static class E
{
public static void M(this IEnumerable<int> x) { }
public static void M(this ReadOnlySpan<int> x) { }
}
La regola consente anche di introdurre nuove API che generano in precedenza ambiguità, ad esempio:
using System;
using System.Collections.Generic;
C.M(new int[] { 1, 2, 3 }); // would be ambiguous before
static class C
{
public static void M(IEnumerable<int> x) { }
public static void M(ReadOnlySpan<int> x) { } // can be added now
}
Avvertimento
Poiché la regola di ottimizzazione è definita per le conversioni di intervallo che esistono solo in LangVersion >= 14
, gli autori di API non possono aggiungere nuovi overload se vogliono continuare a supportare gli utenti su LangVersion <= 13
.
Ad esempio, se .NET 9 BCL introduce tali overload, gli utenti che eseguono l'aggiornamento a net9.0
TFM ma continuano ad utilizzare una versione di linguaggio precedente si troveranno ad affrontare errori di ambiguità nel codice esistente.
Vedi anche una domanda aperta qui sotto.
Inferenza dei tipi
La sezione della specifica relativa alle inferenze sui tipi viene aggiornata come segue (cambiamenti in grassetto).
12.6.3.9 Inferenze esatte
Una inferenza esatta da un tipo
U
a un altro tipoV
viene eseguita come segue:
- Se
V
è uno dei non fissati,Xᵢ
viene aggiunto al set di limiti esatti perU
.- In caso contrario, i set di
V₁...Vₑ
eU₁...Uₑ
vengono determinati controllando se si applica uno dei casi seguenti:
V
è un tipo di matriceV₁[...]
eU
è un tipo di matriceU₁[...]
dello stesso rangoV
è unSpan<V₁>
eU
è un tipo di matriceU₁[]
o unSpan<U₁>
V
è unReadOnlySpan<V₁>
eU
è un tipo di matriceU₁[]
o unSpan<U₁>
oReadOnlySpan<U₁>
V
è il tipoV₁?
eU
è il tipoU₁
V
è un tipo costruitoC<V₁...Vₑ>
eU
è un tipo costruitoC<U₁...Uₑ>
Se si applica uno di questi casi, viene effettuata un'inferenza esatta da ciascun al corrispondenteUᵢ
.- In caso contrario, non vengono effettuate inferenze.
12.6.3.10 Inferenze del limite inferiore
Un'inferenza del limite inferiore da un tipo
U
un tipo si effettua come segue:
- Se
V
è uno dei senza prefissoXᵢ
,U
viene aggiunto al set di limiti inferiori perXᵢ
.- In caso contrario, se
V
è il tipoV₁?
eU
è il tipoU₁?
, viene eseguita un'inferenza con limite inferiore daU₁
aV₁
.- In caso contrario, i set di
U₁...Uₑ
eV₁...Vₑ
vengono determinati controllando se si applica uno dei casi seguenti:
V
è un tipo di matriceV₁[...]
eU
è un tipo di matriceU₁[...]
dello stesso rangoV
è unSpan<V₁>
eU
è un tipo di matriceU₁[]
o unSpan<U₁>
V
è unReadOnlySpan<V₁>
eU
è un tipo di matriceU₁[]
o unSpan<U₁>
oReadOnlySpan<U₁>
V
è una delleIEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
oIList<V₁>
eU
è un tipo di matrice unidimensionaleU₁[]
V
è un tipoclass
,struct
,interface
odelegate
costruitoC<V₁...Vₑ>
e esiste un tipo unicoC<U₁...Uₑ>
tale cheU
(oppure, seU
è un tipoparameter
, la sua classe base effettiva o qualsiasi membro del suo insieme di interfacce effettive) è identico a,inherits
da (direttamente o indirettamente), o implementa (direttamente o indirettamente)C<U₁...Uₑ>
.- La restrizione "univocità" indica che nel caso dell'interfaccia
C<T>{} class U: C<X>, C<Y>{}
, non viene eseguita alcuna inferenza quando si deduce daU
aC<T>
perchéU₁
potrebbe essereX
oY
.
Se uno di questi casi si applica, viene eseguita un'inferenza da ogniUᵢ
alVᵢ
corrispondente come indicato di seguito:- Se
Uᵢ
non è noto come tipo riferimento, viene eseguita un'inferenza esatta- Altrimenti, se
U
è un tipo di matrice, viene eseguita un'inferenza di limite inferioree l'inferenza dipende dal tipo diV
:
- Se
V
è unSpan<Vᵢ>
, viene eseguita un'inferenza esatta- Se
V
è di tipo array o unReadOnlySpan<Vᵢ>
, allora viene effettuata un'inferenza del limite inferiore- In caso contrario, se
U
è unSpan<Uᵢ>
l'inferenza dipende dal tipo diV
:
- Se
V
è unSpan<Vᵢ>
, viene eseguita un'inferenza esatta- Se
V
è unReadOnlySpan<Vᵢ>
, allora si effettua un'inferenza di limite inferiore- In caso contrario, se
U
è unReadOnlySpan<Uᵢ>
eV
è unReadOnlySpan<Vᵢ>
viene fatto un di inferenza di limite inferiore:- In caso contrario, se
V
èC<V₁...Vₑ>
l'inferenza dipende dal parametro di tipoi-th
diC
:
- Se è covariante, viene eseguita un'inferenza del limite inferiore .
- Se è controvariante, viene effettuata un'inferenza del limite superiore.
- Se è invariante, viene effettuata un'inferenza esatta .
- In caso contrario, non vengono effettuate inferenze.
Non esistono regole per l'inferenza con limite superiore perché non sarebbe possibile applicarle.
L'inferenza del tipo non inizia mai come limite superiore, deve passare attraverso un'inferenza con limite inferiore e un parametro di tipo controvariante.
A causa della regola "se Uᵢ
non è noto come tipo di riferimento, viene eseguita un'inferenza esatta ", l'argomento del tipo di origine non può essere Span
/ReadOnlySpan
(questi non possono essere tipi di riferimento).
Tuttavia, l'inferenza dell'intervallo superiore verrà applicata solo se il tipo di origine fosse un Span
/ReadOnlySpan
, poiché avrebbe regole come:
U
è unSpan<U₁>
eV
è un tipo di matriceV₁[]
o unSpan<V₁>
U
è unReadOnlySpan<U₁>
eV
è un tipo di matriceV₁[]
o unSpan<V₁>
oReadOnlySpan<V₁>
Modifiche radicali
Come qualsiasi proposta che modifica le conversioni di scenari esistenti, questa proposta introduce alcuni nuovi cambiamenti dirompenti. Ecco alcuni esempi:
Chiamando Reverse
su un array
La chiamata a x.Reverse()
in cui x
è un'istanza di tipo T[]
si associava in precedenza a IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>)
, mentre ora si associa a void MemoryExtensions.Reverse<T>(this Span<T>)
.
Sfortunatamente queste API sono incompatibili (la seconda esegue l'inversione direttamente e restituisce void
).
.NET 10 mitiga questo problema aggiungendo un overload specifico dell'array IEnumerable<T> Reverse<T>(this T[])
, consultare https://github.com/dotnet/runtime/issues/107723.
void M(int[] a)
{
foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
foreach (var x in Enumerable.Reverse(a)) { } // workaround
}
Vedere anche:
- https://developercommunity.visualstudio.com/t/Extension-method-SystemLinqEnumerable/10790323
- https://developercommunity.visualstudio.com/t/Compilation-Error-When-Calling-Reverse/10818048
- https://developercommunity.visualstudio.com/t/Version-17131-has-an-obvious-defect-th/10858254
- https://developercommunity.visualstudio.com/t/Visual-Studio-2022-update-breaks-build-w/10856758
- https://github.com/dotnet/runtime/issues/111532
- https://developercommunity.visualstudio.com/t/Backward-compatibility-issue-:-IEnumerab/10896189#T-ND10896782
Riunione di progettazione: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse
Ambiguità
Negli esempi seguenti, l'inferenza del tipo non era riuscita in precedenza per l'overload di Span, ma ora l'inferenza del tipo da array a Span riesce, quindi questi esempi risultano ambigui.
Per risolvere questo problema, gli utenti possono usare .AsSpan()
o autori di API possono usare OverloadResolutionPriorityAttribute
.
var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround
xUnit sta aggiungendo ulteriori sovraccarichi per mitigare questo problema: https://github.com/xunit/xunit/discussions/3021.
Riunione di progettazione: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities
Matrici Covarianti
Gli overload che accettano IEnumerable<T>
funzionano su matrici covarianti, ma gli overload che accettano Span<T>
(che ora preferiamo) non funzionano, perché la conversione dello span genera un ArrayTypeMismatchException
per le matrici covarianti.
Probabilmente, il sovraccarico Span<T>
non dovrebbe esistere, dovrebbe invece utilizzare ReadOnlySpan<T>
.
Per ovviare a questo problema, gli utenti possono utilizzare .AsEnumerable()
, oppure gli autori delle API possono utilizzare OverloadResolutionPriorityAttribute
o aggiungere un sovraccarico ReadOnlySpan<T>
, che è preferito in base alla regola di miglioramento.
string[] s = new[] { "a" };
object[] o = s;
C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround
static class C
{
public static void R<T>(IEnumerable<T> e) => Console.Write(1);
public static void R<T>(Span<T> s) => Console.Write(2);
// another workaround:
public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}
Riunione di progettazione: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays
Preferenza per ReadOnlySpan rispetto a Span
La regola di miglioramento causa la preferenza per i sovraccarichi di ReadOnlySpan rispetto ai sovraccarichi di Span per evitare ArrayTypeMismatchException
negli scenari di array covarianti.
Ciò può causare interruzioni di compilazione in alcuni scenari, ad esempio quando gli overload differiscono in base al tipo restituito:
double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround
static class MemoryMarshal
{
public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}
Vedi https://github.com/dotnet/roslyn/issues/76443.
Alberi delle espressioni
I sovraccarichi che accettano span come MemoryExtensions.Contains
sono preferibili rispetto ai sovraccarichi classici come Enumerable.Contains
, anche all'interno degli alberi delle espressioni, ma i ref struct non sono supportati dal motore dell'interprete.
Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14
Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok
Analogamente, i motori di traduzione come LINQ-to-SQL devono reagire a questo problema se i visitatori dell'albero si aspettano Enumerable.Contains
perché incontreranno invece MemoryExtensions.Contains
.
Vedere anche:
- https://github.com/dotnet/runtime/issues/109757
- https://github.com/dotnet/docs/issues/43952
- https://github.com/dotnet/efcore/issues/35100
- https://github.com/dotnet/csharplang/discussions/8959
Progettare riunioni:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#conversions-in-expression-trees
- https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-01-06.md#ignoring-ref-structs-in-expressions
Conversioni definite dall'utente tramite ereditarietà
Aggiungendo le conversioni implicite di span all'elenco delle conversioni implicite standard, possiamo potenzialmente modificare il comportamento quando sono coinvolte conversioni definite dall'utente in una gerarchia di tipi. Questo esempio mostra il cambiamento, rispetto a uno scenario intero che si comporta già come farà il nuovo comportamento C# 14.
Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior
class Base
{
public void M(Span<string> s)
{
Console.WriteLine("Base");
}
public void M(int i)
{
Console.WriteLine("Base");
}
}
class Derived : Base
{
public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
public static implicit operator Derived(long l) => new Derived();
public void M(Derived s)
{
Console.WriteLine("Derived");
}
}
Vedere anche: https://github.com/dotnet/roslyn/issues/78314
Ricerca del metodo di estensione
Consentendo le conversioni di span implicite nella ricerca del metodo di estensione, possiamo potenzialmente cambiare quale metodo viene risolto dalla risoluzione degli overload.
namespace N1
{
using N2;
public class C
{
public static void M()
{
Span<string> span = new string[0];
span.Test(); // Prints N2 today, N1 tomorrow
}
}
public static class N1Ext
{
public static void Test(this ReadOnlySpan<string> span)
{
Console.WriteLine("N1");
}
}
}
namespace N2
{
public static class N2Ext
{
public static void Test(this Span<string> span)
{
Console.WriteLine("N2");
}
}
}
Domande aperte
Regola di migliorezza senza restrizioni
Dovremmo rendere la regola di miglioramento incondizionata rispetto alla versione di linguaggio LangVersion? Ciò consentirà agli autori di API di aggiungere nuove API Span in cui esistono equivalenti IEnumerable senza interrompere gli utenti in LangVersion precedenti o in altri compilatori o linguaggi (ad esempio, VB). Ciò significa tuttavia che gli utenti potrebbero ottenere un comportamento diverso dopo l'aggiornamento del set di strumenti (senza modificare LangVersion o TargetFramework):
- Il compilatore potrebbe scegliere diversi sovraccarichi (tecnicamente un cambiamento che interrompe, ma si spera che quei sovraccarichi abbiano un comportamento equivalente).
- Altre interruzioni potrebbero verificarsi, sconosciute in questo momento.
Si noti che OverloadResolutionPriorityAttribute
non è in grado di risolvere completamente questo problema perché viene ignorato anche in LangVersions meno recenti.
Tuttavia, dovrebbe essere possibile usarlo per evitare ambiguità da VB in cui l'attributo deve essere riconosciuto.
Ignorare altre conversioni definite dall'utente
È stato definito un set di coppie di tipi per le quali sono presenti conversioni implicite ed esplicite definite dal linguaggio.
Ogni volta che esiste una conversione dell'intervallo definito dal linguaggio da T1
a T2
, qualsiasi conversione definita dall'utente da T1
a T2
viene ignorata (indipendentemente dall'intervallo e dalla conversione definita dall'utente in modo implicito o esplicito).
Si noti che include tutte le condizioni, pertanto, ad esempio, non esiste alcuna conversione di intervalli da Span<object>
a ReadOnlySpan<string>
(esiste una conversione di intervallo da Span<T>
a ReadOnlySpan<U>
, ma deve contenere tale T : U
), pertanto una conversione definita dall'utente verrebbe considerata tra tali tipi se esistesse (che avrebbe dovuto essere una conversione specializzata come Span<T>
in ReadOnlySpan<string>
perché gli operatori di conversione non possono avere parametri generici).
È consigliabile ignorare le conversioni definite dall'utente anche tra altre combinazioni di tipi array/Span/ReadOnlySpan/string in cui non esiste alcuna conversione span definita dal linguaggio corrispondente?
Ad esempio, se è presente una conversione definita dall'utente da ReadOnlySpan<T>
a Span<T>
, è consigliabile ignorarla?
Possibilità specifiche da considerare:
-
Ogni volta che esiste una conversione span da
T1
aT2
, ignorare qualsiasi conversione definita dall'utente daT1
aT2
o daT2
aT1
. -
Le conversioni definite dall'utente non vengono considerate durante la conversione tra
- qualsiasi
array_type
eSystem.Span<T>
/System.ReadOnlySpan<T>
unidimensionale, - qualsiasi combinazione di
System.Span<T>
/System.ReadOnlySpan<T>
, -
string
eSystem.ReadOnlySpan<char>
.
- qualsiasi
- Come sopra ma sostituendo l'ultimo punto elenco con:
-
string
eSystem.Span<char>
/System.ReadOnlySpan<char>
.
-
- Come sopra ma sostituendo l'ultimo punto elenco con:
-
string
eSystem.Span<T>
/System.ReadOnlySpan<T>
.
-
Tecnicamente, la specifica non consente di definire alcune di queste conversioni definite dall'utente: non è possibile definire un operatore definito dall'utente tra tipi per i quali esiste una conversione non definita dall'utente (§10.5.2).
Ma Roslyn viola intenzionalmente questa parte della specifica. E alcune conversioni come tra Span
e string
sono consentite comunque (nessuna conversione definita dal linguaggio tra questi tipi esiste).
Comunque, invece di semplicemente ignorare le conversioni, potremmo proibire che siano definite del tutto e forse evitare la violazione delle specifiche almeno per queste nuove conversioni di span, cioè, modificare Roslyn affinché segnali effettivamente un errore di compilazione se queste conversioni sono definite (probabilmente fatta eccezione per quelle già definite dalla BCL).
Alternative
Tenere le cose così come sono.
C# feature specifications