Partilhar via


Tipos de intervalo de primeira classe

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_type unidimensional com tipo de elemento Ei a System.Span<Ei>
  • De qualquer array_type unidimensional com o tipo de elemento Ei a System.ReadOnlySpan<Ui>, desde que Ei seja conversível em covariância (§18.2.3.3) para Ui
  • De System.Span<Ti> a System.ReadOnlySpan<Ui>, desde que Ti seja conversível em covariância (§18.2.3.3) para Ui
  • De System.ReadOnlySpan<Ti> a System.ReadOnlySpan<Ui>, desde que Ti seja conversível em covariância (§18.2.3.3) para Ui
  • De string a System.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 Ti para System.Span<Ui> ou System.ReadOnlySpan<Ui> desde que exista uma conversão de referência explícita de Ti para Ui.

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 identificador
  • Mₑ é 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 de Mₑ. 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 Span como 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 se E não é nulo e pode ser convertido com êxito para o tipo T por 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 T for um tipo de valor não anulável, o resultado será true se D e T forem 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ão E para um tipo T₁, e uma conversão implícita C₂ que converte de uma expressão E para um tipo T₂, C₁ é uma conversão melhor do que C₂ se uma das seguintes condições se mantiver:

  • E é uma expressão de coleção , e C₁ é uma conversão de coleção de expressão melhor do que C₂
  • E não é uma expressão de coleção e uma das seguintes se aplica:
    • E corresponde exatamente T₁ e E não corresponde exatamente a T₂
    • E não corresponde exatamente a nenhum dos T₁ e T₂, e C₁ é uma conversão implícita de span e C₂ não é uma conversão implícita de span
    • E corresponde exatamente a ambos ou a nenhum dos T₁ e T₂, enquanto se aplica a ambos ou a nenhum dos C₁ e C₂ como uma conversão implícita da gama, e T₁ é um melhor alvo de conversão do que T₂.
  • E é um grupo de métodos, T₁ é compatível com o melhor método do grupo de métodos para conversão C₁e T₂ não é compatível com o melhor método único do grupo de métodos para conversão C₂

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₁ e T₂, T₁ é um alvo de conversão melhor do que T₂ se uma das seguintes condições se verificar:

  • T₁ é System.ReadOnlySpan<E₁>, T₂ é System.Span<E₂>e existe uma conversão de identidade de E₁ para E₂
  • T₁ é System.ReadOnlySpan<E₁>, T₂ é System.ReadOnlySpan<E₂>, e existe uma conversão implícita de T₁ para T₂ e não existe conversão implícita de T₂ para T₁
  • Pelo menos um dos T₁ ou T₂ não é System.ReadOnlySpan<Eᵢ> e não é System.Span<Eᵢ>, e existe uma conversão implícita de T₁ para T₂ e não existe nenhuma conversão implícita de T₂ para T₁
  • ...

Reuniões de design:

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 tipo V é feita da seguinte maneira:

  • Se V for um dos Xᵢ não fixos, U será adicionado ao conjunto de limites exatos para Xᵢ.
  • Caso contrário, os conjuntos V₁...Vₑ e U₁...Uₑ são determinados verificando se algum dos seguintes casos se aplica:
    • V é um tipo de matriz V₁[...] e U é um tipo de matriz U₁[...] da mesma classificação
    • V é um Span<V₁> e U é um tipo de matriz U₁[] ou um Span<U₁>
    • V é um ReadOnlySpan<V₁> e U é um tipo de matriz U₁[] ou um Span<U₁> ou ReadOnlySpan<U₁>
    • V é o tipo V₁? e U é o tipo U₁
    • V é um tipo construído C<V₁...Vₑ> e U é um tipo construído C<U₁...Uₑ>
      Se algum destes casos se aplicar, então é feita uma inferência exata de cada Uᵢ para o Vᵢ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 tipo V é feita da seguinte maneira:

  • Se V for um dos Xᵢ não fixos, U será adicionado ao conjunto de limites inferiores para Xᵢ.
  • Caso contrário, se V é o tipo V₁? e U é o tipo U₁? então uma inferência de limite inferior é feita de U₁ para V₁.
  • Caso contrário, os conjuntos U₁...Uₑ e V₁...Vₑ são determinados verificando se algum dos seguintes casos se aplica:
    • V é um tipo de matriz V₁[...] e U é um tipo de matriz U₁[...]da mesma classificação
    • V é um Span<V₁> e U é um tipo de matriz U₁[] ou um Span<U₁>
    • V é um ReadOnlySpan<V₁> e U é um tipo de matriz U₁[] ou um Span<U₁> ou ReadOnlySpan<U₁>
    • V é um dos IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> ou IList<V₁> e U é um tipo de matriz unidimensional U₁[]
    • V é um class construído, struct, interface ou delegate tipo C<V₁...Vₑ> e existe um tipo único C<U₁...Uₑ> tal que U (ou, se U for um tipo parameter, a sua classe base efetiva ou qualquer membro do seu conjunto de interfaces efetivas) é idêntico a, inherits de (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 de U para C<T> porque U₁ pode ser X ou Y.)
      Se qualquer um destes casos se aplicar, então é feita uma inferência de cada Uᵢ para o Vᵢ 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ão uma inferência de limite inferior é feitainferência depende do tipo de V:
      • Se V é um Span<Vᵢ>, então uma inferência exata é feita
      • Se V for do tipo matriz ou um ReadOnlySpan<Vᵢ>, então uma inferência de limite inferior será feita
    • Caso contrário, se U é um Span<Uᵢ> então a inferência depende do tipo de V:
      • Se V é um Span<Vᵢ>, então uma inferência exata é feita
      • Se V é um ReadOnlySpan<Vᵢ>, então é feita uma inferência de limite inferior
    • Caso contrário, se U for um ReadOnlySpan<Uᵢ> e V for um ReadOnlySpan<Vᵢ>, então é feita uma inferência de limite inferior .
    • Caso contrário, se V for C<V₁...Vₑ> então a inferência depende do parâmetro de tipo i-th de C:
      • 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 é um Span<U₁> e V é um tipo de matriz V₁[] ou um Span<V₁>
  • U é um ReadOnlySpan<U₁> e V é um tipo de matriz V₁[] ou um Span<V₁> ou ReadOnlySpan<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:

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:

Reuniões de design:

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:

  1. Sempre que existir uma conversão de span de T1 para T2, ignore qualquer conversão definida pelo usuário de T1 para T2ou de T2 para T1.

  2. As conversões definidas pelo usuário não são consideradas ao converter entre

    • qualquer elemento ou entidade array_type unidimensional e System.Span<T>/System.ReadOnlySpan<T>,
    • qualquer combinação de System.Span<T>/System.ReadOnlySpan<T>,
    • string e System.ReadOnlySpan<char>.
  3. Como acima, mas substituindo o último ponto por:
    • string e System.Span<char>/System.ReadOnlySpan<char>.
  4. Como acima, mas substituindo o último ponto por:
    • string e System.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.