Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de linguagem (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Edição campeã: https://github.com/dotnet/csharplang/issues/8714
Resumo
Introduzimos suporte de primeira classe para Span<T> e ReadOnlySpan<T> na linguagem, incluindo novos tipos de conversão implícita e consideramo-los em mais lugares, permitindo uma programação mais natural com esses tipos integrais.
Motivação
Desde sua introdução no C# 7.2, Span<T> e ReadOnlySpan<T> têm trabalhado seu caminho para a linguagem e biblioteca de classe base (BCL) de muitas maneiras importantes. Isso é ótimo para desenvolvedores, pois sua introdução melhora o desempenho sem custar a segurança do desenvolvedor. No entanto, a linguagem manteve esses tipos em pé de igualdade de algumas maneiras importantes, o que torna difícil expressar a intenção das APIs e leva a uma quantidade significativa de duplicação de área de superfície para novas APIs. Por exemplo, a BCL adicionou várias novas APIs primitivas de tensor no .NET 9, mas todas essas APIs são oferecidas no ReadOnlySpan<T>. O C# não reconhece a relação entre ReadOnlySpan<T>, Span<T>e T[], portanto, embora haja conversões definidas pelo usuário entre esses tipos, elas não podem ser usadas para recetores de método de extensão, não podem compor com outras conversões definidas pelo usuário e não ajudam em todos os cenários genéricos de inferência de tipo.
Os usuários precisariam usar conversões explícitas ou argumentos de tipo, o que significa que as ferramentas do IDE não estão orientando os usuários a usar essas APIs, já que nada indicará ao IDE que é válido passar esses tipos após a conversão. Para proporcionar a ótima usabilidade para este estilo de API, a BCL terá que definir um conjunto inteiro de sobrecargas Span<T> e T[], o que representa uma grande quantidade de trabalho duplicado a manter, sem nenhum benefício real. Esta proposta procura resolver o problema fazendo com que a linguagem reconheça mais diretamente esses tipos e conversões.
Por exemplo, a BCL só pode adicionar uma sobrecarga de qualquer função auxiliar de MemoryExtensions como:
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);
}
Anteriormente, seriam necessárias sobrecargas de Span e array para tornar o método de extensão utilizável em variáveis do tipo Span/array, porque as conversões definidas pelo utilizador (que existem entre Span/array/ReadOnlySpan) não são consideradas para receptores de extensões.
Design Detalhado
As alterações desta proposta estarão ligadas a LangVersion >= 14.
Conversões de span
Adicionamos um novo tipo de conversão implícita à lista em §10.2.1, uma conversão implícita de intervalo . Esta operação consiste numa conversão de tipo e é definida da seguinte forma:
Uma conversão de span implícita permite que array_types, System.Span<T>, System.ReadOnlySpan<T>e string sejam convertidos entre si da seguinte forma:
- De qualquer
array_typeunidimensional com tipo de elementoEiaSystem.Span<Ei> - De qualquer
array_typeunidimensional com o tipo de elementoEiaSystem.ReadOnlySpan<Ui>, desde queEiseja conversível em covariância (§18.2.3.3) paraUi - De
System.Span<Ti>aSystem.ReadOnlySpan<Ui>, desde queTiseja conversível em covariância (§18.2.3.3) paraUi - De
System.ReadOnlySpan<Ti>aSystem.ReadOnlySpan<Ui>, desde queTiseja conversível em covariância (§18.2.3.3) paraUi - De
stringaSystem.ReadOnlySpan<char>
Todos os tipos Span/ReadOnlySpan são considerados aplicáveis para a conversão se forem ref structs e corresponderem pelo seu nome completamente qualificado (LDM 2024-06-24).
Também adicionamos conversão de span implícita à lista de conversões implícitas padrão (§10.4.2). Isso permite que a resolução de sobrecarga os considere ao executar a resolução de argumentos, como na proposta de API vinculada anteriormente.
As conversões de span explícitas são as seguintes:
- Todas as conversões implícitas de intervalo.
- De um array_type com tipo de elemento
TiparaSystem.Span<Ui>ouSystem.ReadOnlySpan<Ui>desde que exista uma conversão de referência explícita deTiparaUi.
Não existe uma conversão de span explícita padrão, ao contrário de outras conversões explícitas padrão (§10.4.3) que sempre existem considerando a conversão implícita padrão oposta.
Conversões definidas pelo usuário
As conversões definidas pelo usuário não são consideradas ao converter entre tipos para os quais existe uma conversão de extensão implícita ou explícita.
As conversões de span implícitas estão isentas da regra de que não é possível definir um operador definido pelo utilizador entre tipos para os quais existe uma conversão não definida pelo utilizador (§10.5.2 Conversões permitidas definidas pelo utilizador). Isso é necessário para que a BCL possa continuar definindo os operadores de conversão Span existentes, mesmo quando eles mudam para C# 14 (eles ainda são necessários para LangVersions mais baixos e também porque esses operadores são usados no codegen das novas conversões de span padrão). Mas pode ser visto como um detalhe de implementação (codegen e LangVersions inferiores não fazem parte da especificação) e Roslyn viola essa parte da especificação de qualquer maneira (esta regra específica sobre conversões definidas pelo usuário não é imposta).
Recetor de extensão
Também adicionamos conversão de span implícita à lista de conversões implícitas aceitáveis no primeiro parâmetro de um método de extensão ao determinar a aplicabilidade (12.8.9.3) (alteração em negrito):
Um método de extensão
Cᵢ.Mₑé elegível se:
Cᵢé uma classe não genérica e não aninhada- O nome do
Mₑé o identificadorMₑé acessível e aplicável quando aplicado aos argumentos como um método estático, como mostrado acima- Uma identidade implícita, de referência
, ou boxing, ou conversão de boxing ou span, existe do expr para o tipo do primeiro parâmetro deMₑ. A conversão de span não é considerada quando a resolução de sobrecarga é realizada para uma conversão de grupo de métodos.
Observe que a conversão de span implícita não é considerada para o recetor de extensão em conversões de grupo de método (LDM 2024-07-15), o que faz com que o código a seguir continue funcionando, em vez de resultar em um erro em tempo de compilação 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);
}
Como possível trabalho futuro, poderíamos considerar a remoção da condição de que a conversão de intervalo não é aplicada ao recetor de extensão em conversões de grupos de métodos. Em vez disso, poderíamos implementar alterações para que o cenário como o mencionado acima acabasse por chamar com sucesso a sobrecarga Span.
- O compilador poderia emitir um thunk que tomaria o array como receptor e realizaria a conversão de span internamente (de maneira semelhante a quando o utilizador cria manualmente o delegado como em
x => new int[0].M(x)). - Os delegados de valor, se implementados, poderiam ser capazes de tomar o
Spancomo recetor diretamente.
Desvio
O objetivo da secção de variância em de conversão de alcance implícito é replicar alguma quantidade de covariância para System.ReadOnlySpan<T>. Alterações de tempo de execução seriam necessárias para implementar totalmente a variância por meio de genéricos aqui (veja .. /csharp-13.0/ref-struct-interfaces.md para usar ref struct tipos em genéricos), mas podemos permitir uma quantidade limitada de covariância através do uso de uma API .NET 9 proposta: https://github.com/dotnet/runtime/issues/96952. Isso permitirá que o idioma trate System.ReadOnlySpan<T> como se o T fosse declarado como out T em alguns cenários. No entanto, não canalizamos esta conversão de variante através de todos os cenários de variância e não a adicionamos à definição de variância-convertível no §18.2.3.3. Se, no futuro, mudarmos o ambiente de execução para compreender mais profundamente a variância aqui, podemos implementar uma pequena mudança disruptiva para integrá-la completamente na linguagem de programação.
Padrões
Observe que quando ref structs são usados como um tipo em qualquer padrão, somente conversões de identidade são permitidas:
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 { }
A partir da especificação de o operador do tipo is (§12.12.12.1):
O resultado da operação
E is T[...] é um valor booleano que indica seEnão é nulo e pode ser convertido com êxito para o tipoTpor uma conversão de referência, uma conversão de boxing, uma conversão de unboxing, uma conversão de encapsulamento ou uma conversão de desempacotamento.[...]
Se
Tfor um tipo de valor não anulável, o resultado serátrueseDeTforem do mesmo tipo.
Esse comportamento não muda com esse recurso, portanto, não será possível escrever padrões para Span/ReadOnlySpan, embora padrões semelhantes sejam possíveis para matrizes (incluindo variância):
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
}
Geração de código
As conversões sempre existirão, independentemente da presença ou não de auxiliares de tempo de execução usados para implementá-las (LDM 2024-05-13). Se os ajudantes não estiverem presentes, tentar usar a conversão resultará em um erro em tempo de compilação que um membro necessário pelo compilador está ausente.
O compilador espera usar os seguintes auxiliares ou equivalentes para implementar as conversões:
| Conversão | Ajudantes |
|---|---|
| array para Span |
static implicit operator Span<T>(T[]) (definido em Span<T>) |
| array para ReadOnlySpan |
static implicit operator ReadOnlySpan<T>(T[]) (definido em ReadOnlySpan<T>) |
| Span para ReadOnlySpan |
static implicit operator ReadOnlySpan<T>(Span<T>) (definido em Span<T>) e static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
| ReadOnlySpan para ReadOnlySpan | static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
| Conversão de string para ReadOnlySpan | static ReadOnlySpan<char> MemoryExtensions.AsSpan(string) |
Observe que MemoryExtensions.AsSpan é usado em vez do operador implícito equivalente definido em string.
Isso significa que o codegen é diferente entre LangVersions (o operador implícito é usado em C# 13; o método estático AsSpan é usado em C# 14).
Por outro lado, a conversão pode ser emitida no .NET Framework (o método AsSpan existe lá, enquanto o operador string não).
A conversão de matriz explícita para (ReadOnly)Span primeiro converte explicitamente da matriz de origem para uma matriz com o tipo de elemento de destino e, em seguida, para (ReadOnly)Span através da mesma funcionalidade auxiliar que uma conversão implícita usaria, ou seja, o op_Implicit(T[])correspondente.
Melhor conversão a partir da expressão
Melhor conversão da expressão (§12.6.4.5) é atualizada de forma a privilegiar conversões de intervalo implícito. Isso baseia-se nas alterações de resolução de sobrecarga de expressões de coleção .
Dada uma conversão implícita
C₁que converte de uma expressãoEpara um tipoT₁, e uma conversão implícitaC₂que converte de uma expressãoEpara um tipoT₂,C₁é uma conversão melhor do queC₂se uma das seguintes condições se mantiver:
Eé uma expressão de coleção , eC₁é uma conversão de coleção de expressão melhor do queC₂Enão é uma expressão de coleção e uma das seguintes se aplica:
Ecorresponde exatamenteT₁eEnão corresponde exatamente aT₂Enão corresponde exatamente a nenhum dosT₁eT₂, eC₁é uma conversão implícita de span eC₂não é uma conversão implícita de spanEcorresponde exatamente a ambos ou a nenhum dosT₁eT₂, enquanto se aplica a ambos ou a nenhum dosC₁eC₂como uma conversão implícita da gama, eT₁é um melhor alvo de conversão do queT₂.Eé um grupo de métodos,T₁é compatível com o melhor método do grupo de métodos para conversãoC₁eT₂não é compatível com o melhor método único do grupo de métodos para conversãoC₂
Melhor meta de conversão
Melhor alvo de conversão (§12.6.4.7) é atualizado para preferir ReadOnlySpan<T> em vez de Span<T>.
Dado que existem dois tipos
T₁eT₂,T₁é um alvo de conversão melhor do queT₂se uma das seguintes condições se verificar:
T₁éSystem.ReadOnlySpan<E₁>,T₂éSystem.Span<E₂>e existe uma conversão de identidade deE₁paraE₂T₁éSystem.ReadOnlySpan<E₁>,T₂éSystem.ReadOnlySpan<E₂>, e existe uma conversão implícita deT₁paraT₂e não existe conversão implícita deT₂paraT₁- Pelo menos um dos
T₁ouT₂não éSystem.ReadOnlySpan<Eᵢ>e não éSystem.Span<Eᵢ>, e existe uma conversão implícita deT₁paraT₂e não existe nenhuma conversão implícita deT₂paraT₁- ...
Reuniões de design:
- 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
Observações sobre a melhoria
A conversão melhor da regra de expressão deve garantir que, sempre que uma sobrecarga se torne aplicável devido às novas conversões de extensão, qualquer ambiguidade potencial com outra sobrecarga seja evitada porque a sobrecarga recém-aplicável é preferida.
Sem essa regra, o código a seguir compilado com êxito em C# 13 resultaria em um erro de ambiguidade em C# 14 devido à nova conversão implícita padrão de array para ReadOnlySpan aplicável a um recetor de método de extensão:
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) { }
}
A regra também permite a introdução de novas APIs que anteriormente resultariam em ambiguidades, por exemplo:
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
}
Advertência
Como a regra de superioridade é definida para conversões de span que existem apenas no LangVersion >= 14, os autores da API não podem adicionar essas novas sobrecargas se quiserem continuar suportando os utilizadores em LangVersion <= 13.
Por exemplo, se a BCL do .NET 9 introduzir essas sobrecargas, os utilizadores que atualizarem para net9.0 TFM, mas permanecerem numa LangVersion inferior, receberão erros de ambiguidade para o código existente.
Ver também uma pergunta aberta infra.
Inferência de tipo
Atualizamos a seção de inferências de tipo da especificação da seguinte forma (alterações em negrito ).
12.6.3.9 Inferências exatas
Uma inferência exata de um tipo
Upara um tipoVé feita da seguinte maneira:
- Se
Vfor um dosXᵢnão fixos,Userá adicionado ao conjunto de limites exatos paraXᵢ.- Caso contrário, os conjuntos
V₁...VₑeU₁...Uₑsão determinados verificando se algum dos seguintes casos se aplica:
Vé um tipo de matrizV₁[...]eUé um tipo de matrizU₁[...]da mesma classificaçãoVé umSpan<V₁>eUé um tipo de matrizU₁[]ou umSpan<U₁>Vé umReadOnlySpan<V₁>eUé um tipo de matrizU₁[]ou umSpan<U₁>ouReadOnlySpan<U₁>Vé o tipoV₁?eUé o tipoU₁Vé um tipo construídoC<V₁...Vₑ>eUé um tipo construídoC<U₁...Uₑ>
Se algum destes casos se aplicar, então é feita uma inferência exata de cadaUᵢpara oVᵢcorrespondente.- Caso contrário, não são feitas inferências.
12.6.3.10 Inferências de limite inferior
Uma inferência de limite inferior de um tipo
Upara um tipoVé feita da seguinte maneira:
- Se
Vfor um dosXᵢnão fixos,Userá adicionado ao conjunto de limites inferiores paraXᵢ.- Caso contrário, se
Vé o tipoV₁?eUé o tipoU₁?então uma inferência de limite inferior é feita deU₁paraV₁.- Caso contrário, os conjuntos
U₁...UₑeV₁...Vₑsão determinados verificando se algum dos seguintes casos se aplica:
Vé um tipo de matrizV₁[...]eUé um tipo de matrizU₁[...]da mesma classificaçãoVé umSpan<V₁>eUé um tipo de matrizU₁[]ou umSpan<U₁>Vé umReadOnlySpan<V₁>eUé um tipo de matrizU₁[]ou umSpan<U₁>ouReadOnlySpan<U₁>Vé um dosIEnumerable<V₁>,ICollection<V₁>,IReadOnlyList<V₁>>,IReadOnlyCollection<V₁>ouIList<V₁>eUé um tipo de matriz unidimensionalU₁[]Vé umclassconstruído,struct,interfaceoudelegatetipoC<V₁...Vₑ>e existe um tipo únicoC<U₁...Uₑ>tal queU(ou, seUfor um tipoparameter, a sua classe base efetiva ou qualquer membro do seu conjunto de interfaces efetivas) é idêntico a,inheritsde (direta ou indiretamente), ou implementa (direta ou indiretamente)C<U₁...Uₑ>.- (A restrição de "unicidade" significa que, no caso da interface
C<T>{} class U: C<X>, C<Y>{}, então nenhuma inferência é feita ao inferir deUparaC<T>porqueU₁pode serXouY.)
Se qualquer um destes casos se aplicar, então é feita uma inferência de cadaUᵢpara oVᵢcorrespondente da seguinte forma:- Se
Uᵢnão é conhecido por ser um tipo de referência, então uma inferência exata é feita- Caso contrário, se
Ué um tipo de matriz, entãouma inferência de limite inferior é feitainferência depende do tipo deV:
- Se
Vé umSpan<Vᵢ>, então uma inferência exata é feita- Se
Vfor do tipo matriz ou umReadOnlySpan<Vᵢ>, então uma inferência de limite inferior será feita- Caso contrário, se
Ué umSpan<Uᵢ>então a inferência depende do tipo deV:
- Se
Vé umSpan<Vᵢ>, então uma inferência exata é feita- Se
Vé umReadOnlySpan<Vᵢ>, então é feita uma inferência de limite inferior- Caso contrário, se
Ufor umReadOnlySpan<Uᵢ>eVfor umReadOnlySpan<Vᵢ>, então é feita uma inferência de limite inferior .- Caso contrário, se
VforC<V₁...Vₑ>então a inferência depende do parâmetro de tipoi-thdeC:
- Se for covariante, então é feita uma inferência de limite inferior .
- Se for contravariante, então é feita uma inferência de limite superior.
- Se for invariante, então é feita uma inferência exata.
- Caso contrário, não são feitas inferências.
Não há regras para inferência de limite superior porque não seria possível atingi-las.
A inferência de tipo nunca começa como limite superior, teria que passar por uma inferência de limite inferior e um parâmetro de tipo contravariante.
Devido à regra "se Uᵢ não é conhecido por ser um tipo de referência, então uma inferência exata é feita", o argumento do tipo de fonte não pôde ser Span/ReadOnlySpan (esses não podem ser tipos de referência).
No entanto, a inferência de extensão de limite superior só se aplicaria se o tipo de fonte fosse um Span/ReadOnlySpan, uma vez que teria regras como as seguintes:
Ué umSpan<U₁>eVé um tipo de matrizV₁[]ou umSpan<V₁>Ué umReadOnlySpan<U₁>eVé um tipo de matrizV₁[]ou umSpan<V₁>ouReadOnlySpan<V₁>
Alterações de grande impacto
Como qualquer proposta que altere as conversões de cenários existentes, esta proposta introduz novas alterações importantes. Eis alguns exemplos:
Chamando Reverse em uma matriz
Chamar x.Reverse() onde x é uma instância do tipo T[] anteriormente se ligaria a IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>), enquanto agora se liga a void MemoryExtensions.Reverse<T>(this Span<T>).
Infelizmente, essas APIs são incompatíveis (a última faz a reversão diretamente e retorna o valor void).
O .NET 10 atenua isso adicionando uma sobrecarga específica da matriz IEnumerable<T> Reverse<T>(this T[]), consulte 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
}
Ver também:
- 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
Reunião de design: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse
Ambiguidades
Os exemplos a seguir falharam anteriormente na inferência de tipo para a sobrecarga Span, mas agora a inferência de tipo de matriz para Span é bem-sucedida, portanto, estes são ambíguos.
Para contornar isso, os usuários podem usar .AsSpan() ou os autores da API podem usar 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 está a adicionar mais sobrecargas para mitigar isso: https://github.com/xunit/xunit/discussions/3021.
Reunião de design: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities
Matrizes covariantes
As sobrecargas que aceitam IEnumerable<T> funcionavam com matrizes covariantes, mas as sobrecargas que aceitam Span<T> (que agora preferimos) não funcionam, porque a conversão de span lança um ArrayTypeMismatchException para matrizes covariantes.
Argumentavelmente, a sobrecarga Span<T> não deveria existir; em vez disso, deveria utilizar ReadOnlySpan<T>.
Para contornar isso, os utilizadores podem usar .AsEnumerable(), ou os autores de API podem usar OverloadResolutionPriorityAttribute ou adicionar a sobrecarga ReadOnlySpan<T>, que é preferida devido à regra de melhoria de.
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);
}
Reunião de design: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays
Preferindo ReadOnlySpan em vez de Span
A regra de melhoria faz com que se prefiram as sobrecargas ReadOnlySpan em vez das sobrecargas Span, para evitar ArrayTypeMismatchExceptions em cenários de matrizes covariantes .
Isso pode levar a quebras de compilação em alguns cenários, por exemplo, quando as sobrecargas diferem por seu tipo de retorno:
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;
}
Consulte https://github.com/dotnet/roslyn/issues/76443.
Árvores de expressão
Sobrecargas com intervalos como MemoryExtensions.Contains são preferíveis a sobrecargas clássicas como Enumerable.Contains, mesmo dentro de árvores de expressão - mas as 'ref structs' não são suportadas pelo motor de interpretação:
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
Da mesma forma, mecanismos de tradução como LINQ-to-SQL precisam reagir a isso se seus visitantes da árvore esperam Enumerable.Contains porque eles encontrarão MemoryExtensions.Contains em vez disso.
Ver também:
- 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
Reuniões de design:
- 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
Conversões definidas pelo utilizador por meio de herança
Ao adicionar conversões de intervalos implícitos à lista de conversões implícitas padrão, podemos, potencialmente, alterar o comportamento quando conversões definidas pelo usuário estão envolvidas numa hierarquia de tipos. Este exemplo mostra essa alteração, comparado a um cenário inteiro que já se comporta da mesma forma que o novo comportamento do 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");
}
}
Veja também: https://github.com/dotnet/roslyn/issues/78314
Procura de método de extensão
Ao permitir conversões implícitas de intervalo na pesquisa do método de extensão, podemos potencialmente alterar qual método de extensão é resolvido pela resolução de sobrecarga.
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");
}
}
}
Perguntas abertas
Regra de melhoria irrestrita
Devemos tornar a regra da melhoria incondicional ao LangVersion? Isso permitiria que os autores da API adicionassem novas APIs Span onde existam equivalentes IEnumerable, sem prejudicar os utilizadores em LangVersions mais antigas ou outros compiladores ou linguagens (por exemplo, VB). No entanto, isso significaria que os usuários poderiam obter um comportamento diferente depois de atualizar o conjunto de ferramentas (sem alterar LangVersion ou TargetFramework):
- O compilador poderia escolher diferentes sobrecargas (tecnicamente uma mudança significativa, mas com sorte essas sobrecargas teriam um comportamento equivalente).
- Outras interrupções poderiam surgir, desconhecidas neste momento.
Observe que OverloadResolutionPriorityAttribute não pode resolver totalmente isso porque também é ignorado em LangVersions mais antigas.
No entanto, deve ser possível usá-lo para evitar ambiguidades do VB onde o atributo deve ser reconhecido.
Ignorando outras conversões definidas pelo utilizador
Definimos um conjunto de pares de tipos para os quais existem conversões de span implícitas e explícitas definidas pelo idioma.
Sempre que existe uma conversão de intervalo definida pela linguagem de T1 para T2, qualquer conversão definida pelo utilizador de T1 para T2 é ignorada (independentemente do intervalo e da conversão definida pelo utilizador ser implícita ou explícita).
Observe que isto abrange todas as condições; por exemplo, não há conversão de span de Span<object> para ReadOnlySpan<string> (há uma conversão de span de Span<T> para ReadOnlySpan<U>, mas deve manter que T : U). Assim, uma conversão definida pelo utilizador seria considerada entre esses tipos se existisse, e deveria ser uma conversão especializada como de Span<T> para ReadOnlySpan<string>, porque os operadores de conversão não podem ter parâmetros genéricos.
Devemos ignorar conversões definidas pelo usuário também entre outras combinações de tipos de array/Span/ReadOnlySpan/string onde não existe nenhuma conversão de span correspondente definida pelo idioma?
Por exemplo, se houver uma conversão definida pelo usuário de ReadOnlySpan<T> para Span<T>, devemos ignorá-la?
Especificações de possibilidades a serem consideradas:
-
Sempre que existir uma conversão de span de
T1paraT2, ignore qualquer conversão definida pelo usuário deT1paraT2ou deT2paraT1. -
As conversões definidas pelo usuário não são consideradas ao converter entre
- qualquer elemento ou entidade
array_typeunidimensional eSystem.Span<T>/System.ReadOnlySpan<T>, - qualquer combinação de
System.Span<T>/System.ReadOnlySpan<T>, -
stringeSystem.ReadOnlySpan<char>.
- qualquer elemento ou entidade
- Como acima, mas substituindo o último ponto por:
-
stringeSystem.Span<char>/System.ReadOnlySpan<char>.
-
- Como acima, mas substituindo o último ponto por:
-
stringeSystem.Span<T>/System.ReadOnlySpan<T>.
-
Tecnicamente, a especificação não permite que algumas dessas conversões definidas pelo usuário sejam sequer definidas: não é possível definir um operador definido pelo usuário entre tipos para os quais existe uma conversão não definida pelo usuário (§10.5.2).
Mas Roslyn viola intencionalmente esta parte da especificação. E algumas conversões, como entre Span e string, são permitidas de qualquer maneira (não existe nenhuma conversão definida pelo idioma entre esses tipos).
No entanto, em vez de apenas ignorar as conversões, poderíamos proibir que elas fossem definidas de todo e talvez sair da violação de especificação pelo menos para essas novas conversões de intervalo, ou seja, alterar Roslyn para realmente reportar um erro de compilação se essas conversões forem definidas (provavelmente exceto aquelas já definidas pela BCL).
Alternativas
Mantenha as coisas como estão.
C# feature specifications