Condividi tramite


Tipi di intervallo di prima classe

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 elemento Ei a System.Span<Ei>
  • Da qualsiasi array_type unidimensionale con tipo di elemento Ei a System.ReadOnlySpan<Ui>, a condizione che Ei sia convertibile con covarianza (§18.2.3.3) a Ui
  • Da System.Span<Ti> a System.ReadOnlySpan<Ui>, a condizione che Ti sia covarianza convertibile (§18.2.3.3) a Ui
  • Da System.ReadOnlySpan<Ti> a System.ReadOnlySpan<Ui>, a condizione che Ti sia covarianza convertibile (§18.2.3.3) a Ui
  • Da string a System.ReadOnlySpan<char>

Qualsiasi tipo Span/ReadOnlySpan viene considerato applicabile per la conversione se sono ref structe 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 a System.Span<Ui> o System.ReadOnlySpan<Ui>, purché esista una conversione di riferimento esplicita da Ti a Ui.

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ₑ è identifier
  • Mₑ è 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 di Mₑ. 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 structsono 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 se E è diverso da null e può essere convertito correttamente in un tipo T 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 se D e T 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'espressione E a un tipo T₁e una conversione implicita C₂ che converte da un'espressione E a un tipo T₂, C₁ è una conversione migliore rispetto a C₂ se si verifica una delle seguenti condizioni:

  • E è un'espressione di raccolta , e C₁ è una conversione di raccolta di espressioni.
  • E non è un'espressione di raccolta e vale una delle seguenti condizioni:
    • E corrisponde esattamente T₁ e E non corrisponde esattamente T₂
    • E non corrisponde esattamente né a T₁ né a T₂, e C₁ è una conversione implicita dello span e C₂ non è una conversione implicita dello span
    • E corrisponde esattamente sia a T₁ sia a T₂, oppure a nessuna delle due, sia C₁ che C₂ sono una conversione implicita dell'intervallo, e T₁ è un miglior obiettivo di conversione rispetto a T₂.
  • E è un gruppo di metodi, T₁ è compatibile con il singolo metodo migliore del gruppo di metodi per la conversione C₁e T₂ non è compatibile con il singolo metodo migliore del gruppo di metodi per la conversione C₂

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₁ e T₂, T₁ è una migliore destinazione di conversione rispetto a T₂ se si verifica una delle seguenti condizioni:

  • T₁ è System.ReadOnlySpan<E₁>, T₂ è System.Span<E₂>e esiste una conversione di identità da E₁ a E₂
  • T₁ è System.ReadOnlySpan<E₁>, T₂ è System.ReadOnlySpan<E₂>e esiste una conversione implicita da T₁ a T₂ e non esiste alcuna conversione implicita da T₂ a T₁ esiste
  • Almeno uno di T₁ o T₂ non è System.ReadOnlySpan<Eᵢ> e non è System.Span<Eᵢ>e una conversione implicita da T₁ a T₂ esiste e non esiste alcuna conversione implicita da T₂ a T₁
  • ...

Progettare riunioni:

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 Ua un altro tipo V viene eseguita come segue:

  • Se V è uno dei non fissati, Xᵢ viene aggiunto al set di limiti esatti per U.
  • In caso contrario, i set di V₁...Vₑ e U₁...Uₑ vengono determinati controllando se si applica uno dei casi seguenti:
    • V è un tipo di matrice V₁[...] e U è un tipo di matrice U₁[...] dello stesso rango
    • V è un Span<V₁> e U è un tipo di matrice U₁[] o un Span<U₁>
    • V è un ReadOnlySpan<V₁> e U è un tipo di matrice U₁[] o un Span<U₁> o ReadOnlySpan<U₁>
    • V è il tipo V₁? e U è il tipo U₁
    • V è un tipo costruito C<V₁...Vₑ> e U è un tipo costruito C<U₁...Uₑ>
      Se si applica uno di questi casi, viene effettuata un'inferenza esatta da ciascun al corrispondente Uᵢ.
  • 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 per Xᵢ.
  • In caso contrario, se V è il tipo V₁? e U è il tipo U₁?, viene eseguita un'inferenza con limite inferiore da U₁ a V₁.
  • In caso contrario, i set di U₁...Uₑ e V₁...Vₑ vengono determinati controllando se si applica uno dei casi seguenti:
    • V è un tipo di matrice V₁[...] e U è un tipo di matrice U₁[...]dello stesso rango
    • V è un Span<V₁> e U è un tipo di matrice U₁[] o un Span<U₁>
    • V è un ReadOnlySpan<V₁> e U è un tipo di matrice U₁[] o un Span<U₁> o ReadOnlySpan<U₁>
    • V è una delle IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> o IList<V₁> e U è un tipo di matrice unidimensionale U₁[]
    • V è un tipo class, struct, interface o delegate costruito C<V₁...Vₑ> e esiste un tipo unico C<U₁...Uₑ> tale che U (oppure, se U è un tipo parameter, 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 da U a C<T> perché U₁ potrebbe essere X o Y.
      Se uno di questi casi si applica, viene eseguita un'inferenza da ogni Uᵢ al Vᵢ 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 inferiore e l'inferenza dipende dal tipo di V:
      • Se V è un Span<Vᵢ>, viene eseguita un'inferenza esatta
      • Se V è di tipo array o un ReadOnlySpan<Vᵢ>, allora viene effettuata un'inferenza del limite inferiore
    • In caso contrario, se U è un Span<Uᵢ> l'inferenza dipende dal tipo di V:
      • Se V è un Span<Vᵢ>, viene eseguita un'inferenza esatta
      • Se V è un ReadOnlySpan<Vᵢ>, allora si effettua un'inferenza di limite inferiore
    • In caso contrario, se U è un ReadOnlySpan<Uᵢ> e V è un ReadOnlySpan<Vᵢ> viene fatto un di inferenza di limite inferiore:
    • In caso contrario, se V è C<V₁...Vₑ> l'inferenza dipende dal parametro di tipo i-th di C:
      • 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 è un Span<U₁> e V è un tipo di matrice V₁[] o un Span<V₁>
  • U è un ReadOnlySpan<U₁> e V è un tipo di matrice V₁[] o un Span<V₁> o ReadOnlySpan<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:

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:

Progettare riunioni:

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:

  1. Ogni volta che esiste una conversione span da T1 a T2, ignorare qualsiasi conversione definita dall'utente da T1 a T2o da T2 a T1.

  2. Le conversioni definite dall'utente non vengono considerate durante la conversione tra

    • qualsiasi array_type e System.Span<T>/System.ReadOnlySpan<T>unidimensionale,
    • qualsiasi combinazione di System.Span<T>/System.ReadOnlySpan<T>,
    • string e System.ReadOnlySpan<char>.
  3. Come sopra ma sostituendo l'ultimo punto elenco con:
    • string e System.Span<char>/System.ReadOnlySpan<char>.
  4. Come sopra ma sostituendo l'ultimo punto elenco con:
    • string e System.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.