Partilhar via


params Collections

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 Language Design Meeting (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 .

Questão prioritária: https://github.com/dotnet/csharplang/issues/7700

Resumo

Na linguagem C# 12, foi adicionado suporte para a criação de instâncias de tipos de coleção além de apenas matrizes. Veja as expressões da coleção . A presente proposta estende o apoio params a todos esses tipos de coleção.

Motivação

Um parâmetro de matriz params fornece uma maneira conveniente de chamar um método que usa uma lista de comprimento arbitrário de argumentos. Hoje o parâmetro params deve ser do tipo matriz. No entanto, pode ser benéfico para um desenvolvedor poder ter a mesma conveniência ao chamar APIs que usam outros tipos de coleção. Por exemplo, um ImmutableArray<T>, ReadOnlySpan<T>ou IEnumerablesimples. Especialmente nos casos em que o compilador é capaz de evitar uma alocação de matriz implícita com a finalidade de criar a coleção (ImmutableArray<T>, ReadOnlySpan<T>, etc).

Hoje, em situações em que uma API usa um tipo de coleção, os desenvolvedores geralmente adicionam uma sobrecarga de params que recebe uma matriz, constroem a coleção de destino e chamam a sobrecarga original com essa coleção, assim os consumidores da API têm que trocar uma alocação extra de matriz por conveniência.

Outra motivação é a capacidade de adicionar uma sobrecarga de parâmetros variáveis de extensões e garantir que ela tenha prioridade sobre a versão em formato de matriz, simplesmente recompilando o código-fonte existente.

Projeto detalhado

Parâmetros do método

Os parâmetros da secção do Método são ajustados da seguinte forma.

formal_parameter_list
    : fixed_parameters
-    | fixed_parameters ',' parameter_array
+    | fixed_parameters ',' parameter_collection
-    | parameter_array
+    | parameter_collection
    ;

-parameter_array
+parameter_collection
-    : attributes? 'params' array_type identifier
+    : attributes? 'params' 'scoped'? type identifier
    ;

Um parameter_collection consiste em um conjunto opcional de atributos , um modificador de params, um modificador de scoped opcional, um tipo de e um identificador de . Uma coleção de parâmetros declara um único parâmetro do tipo dado com o nome fornecido. O tipo de uma coleção de parâmetros deve ser um dos seguintes tipos de destino válidos para uma expressão de coleção (ver https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#conversions):

  • Um tipo de matriz unidimensionalT[], caso em que o tipo de elemento é T
  • Um intervalo do tipo
    • System.Span<T>
    • System.ReadOnlySpan<T>
      em que casos o tipo de elemento é T
  • Um tipo com um método create apropriado que pode ser invocado sem argumentos adicionais, que é pelo menos tão acessível quanto o membro declarante, e com um tipo de elemento correspondente resultante dessa determinação
  • Um struct ou tipo de classe que implementa System.Collections.IEnumerable onde:
    • O tipo tem um construtor que pode ser invocado sem argumentos, e o construtor é pelo menos tão acessível quanto o membro declarante.

    • O tipo tem um método de instância (não uma extensão) Add onde:

      • O método pode ser invocado com um único argumento de valor.
      • Se o método for genérico, os argumentos de tipo podem ser inferidos a partir do argumento.
      • O método é pelo menos tão acessível quanto o membro declarante.

      Nesse caso, o tipo de elemento é o tipo de iteração do tipo .

  • Tipo de interface
    • System.Collections.Generic.IEnumerable<T>,
    • System.Collections.Generic.IReadOnlyCollection<T>,
    • System.Collections.Generic.IReadOnlyList<T>,
    • System.Collections.Generic.ICollection<T>,
    • System.Collections.Generic.IList<T>
      em que casos o tipo de elemento é T

Em uma invocação de método, uma coleção de parâmetros permite que um único argumento do tipo de parâmetro dado seja especificado ou permite que zero ou mais argumentos do tipo de elemento da coleção sejam especificados. As coleções de parâmetros são descritas mais detalhadamente em Parameter collections.

Uma parameter_collection pode ocorrer após um parâmetro opcional, mas não pode ter um valor padrão – a omissão de argumentos para um parameter_collection resultaria, em vez disso, na criação de uma coleção vazia.

Coleções de parâmetros

A seção matrizes de parâmetros é renomeada e ajustada da seguinte forma.

Um parâmetro declarado com um modificador de params é uma coleção de parâmetros. Se uma lista formal de parâmetros incluir um parâmetro de coleção, este deve ser o último parâmetro da lista e deve ser do tipo especificado na secção parâmetros de método.

Nota: Não é possível combinar o modificador params com os modificadores in, outou ref. nota final

Uma coleção de parâmetros permite que os argumentos sejam especificados de uma das duas maneiras em uma invocação de método:

  • O argumento dado para uma coleção de parâmetros pode ser uma única expressão que é implicitamente conversível para o tipo de coleção de parâmetros. Neste caso, a coleção de parâmetros age precisamente como um parâmetro de valor.
  • Como alternativa, a invocação pode especificar zero ou mais argumentos para a coleção de parâmetros, onde cada argumento é uma expressão que é implicitamente conversível para o tipo de elemento da coleção de parâmetros. Nesse caso, a invocação cria uma instância do tipo de coleção de parâmetros de acordo com as regras especificadas em expressões de coleção, como se os argumentos fossem utilizados como elementos de expressão em uma expressão de coleção na mesma ordem, e usa a instância de coleção recém-criada como o argumento real. Ao construir a instância de coleção, os argumentos originais não convertidos são usados.

Exceto por permitir um número variável de argumentos em uma invocação, uma coleção de parâmetros é precisamente equivalente a um parâmetro de valor do mesmo tipo.

Ao executar a resolução de sobrecarga, um método com uma coleção de parâmetros pode ser aplicável, em sua forma normal ou em sua forma expandida. A forma expandida de um método só está disponível se a forma normal do método não for aplicável e apenas se um método aplicável com a mesma assinatura que o formulário expandido não for já declarado no mesmo tipo.

Uma ambiguidade potencial surge entre a forma normal e a forma expandida do método com um único argumento de coleta de parâmetros quando ele pode ser usado como a própria coleção de parâmetros e como o elemento da coleção de parâmetros ao mesmo tempo. No entanto, a ambiguidade não apresenta problemas, uma vez que pode ser resolvida inserindo um elenco ou usando uma expressão de coleção, se necessário.

Assinaturas e sobrecarga

Todas as regras em torno do modificador params em Assinaturas e sobrecarga permanecem como estão.

Membro da função aplicável

A secção membro da função aplicável é ajustada da seguinte forma.

Se um membro de função que inclui uma coleção de parâmetros não for aplicável em sua forma normal, o membro da função poderá, em vez disso, ser aplicável em sua forma expandida:

  • Se a coleção de parâmetros não for uma matriz, um formulário expandido não será aplicável às versões de idioma C# 12 e inferiores.
  • O formulário expandido é construído substituindo a coleção de parâmetros na declaração de membro da função por zero ou mais parâmetros de valor do tipo de elemento da coleção de parâmetros de modo que o número de argumentos na lista de argumentos A corresponda ao número total de parâmetros. Se A tiver menos argumentos do que o número de parâmetros fixos na declaração do membro da função, a forma expandida do membro da função não pode ser construída e, portanto, não é aplicável.
  • Caso contrário, a forma expandida será aplicável se, para cada argumento em A, uma das seguintes opções for verdadeira:
    • o modo de passagem de parâmetros do argumento é idêntico ao modo de passagem de parâmetros do parâmetro correspondente, e
      • para um parâmetro de valor fixo ou um parâmetro de valor criado pela expansão, existe uma conversão implícita da expressão de argumento para o tipo do parâmetro correspondente, ou
      • Para um parâmetro in, outou ref, o tipo da expressão de argumento é idêntico ao tipo do parâmetro correspondente.
    • O modo de passagem de parâmetros do argumento é valor, e o modo de passagem de parâmetros do parâmetro correspondente é entrada, e existe uma conversão implícita da expressão do argumento para o tipo do parâmetro correspondente

Membro de função melhorado

A secção membro da função Better é ajustada da seguinte forma.

Dada uma lista de argumentos A com um conjunto de expressões de argumento {E₁, E₂, ..., Eᵥ} e dois membros de função aplicáveis Mᵥ e Mₓ com tipos de parâmetros {P₁, P₂, ..., Pᵥ} e {Q₁, Q₂, ..., Qᵥ}, Mᵥ é definido como um membro de função melhor do que Mₓ se

  • para cada argumento, a conversão implícita de Eᵥ para Qᵥ não é melhor do que a conversão implícita de Eᵥ para Pᵥ, e
  • Para pelo menos um argumento, a conversão de Eᵥ para Pᵥ é melhor do que a conversão de Eᵥ para Qᵥ.

Caso as sequências de tipo de parâmetro {P₁, P₂, ..., Pᵥ} e {Q₁, Q₂, ..., Qᵥ} sejam equivalentes (ou seja, cada Pᵢ tem uma conversão de identidade para o Qᵢcorrespondente), as seguintes regras de desempate são aplicadas, a fim de determinar o melhor membro da função.

  • Se Mᵢ é um método não genérico e Mₑ é um método genérico, então Mᵢ é melhor do que Mₑ.
  • Caso contrário, se Mᵢ é aplicável na sua forma normal e Mₑ tem uma coleção de parâmetros e é aplicável apenas na sua forma expandida, então Mᵢ é melhor do que Mₑ.
  • Caso contrário, se ambos os métodos têm coleções de parâmetros e são aplicáveis apenas em suas formas expandidas, e se a coleção de params de Mᵢ tem menos elementos do que a coleção de params de Mₑ, então Mᵢ é melhor do que Mₑ.
  • Caso contrário, se Mᵥ tiver mais tipos de parâmetros específicos do que Mₓ, então Mᵥ é melhor do que Mₓ. Deixe que {R1, R2, ..., Rn} e {S1, S2, ..., Sn} representem os tipos de parâmetros não instanciados e não expandidos de Mᵥ e Mₓ. Mᵥtipos de parâmetros são mais específicos do que Mₓs se, para cada parâmetro, Rx não for menos específico do que Sxe, pelo menos para um parâmetro, Rx for mais específico do que Sx:
    • Um parâmetro type é menos específico do que um parâmetro non-type.
    • Recursivamente, um tipo construído é mais específico do que outro tipo construído (com o mesmo número de argumentos de tipo) se pelo menos um argumento de tipo é mais específico e nenhum argumento de tipo é menos específico do que o argumento de tipo correspondente no outro.
    • Um tipo de matriz é mais específico do que outro tipo de matriz (com o mesmo número de dimensões) se o tipo de elemento do primeiro for mais específico do que o tipo de elemento do segundo.
  • Caso contrário, se um membro for um operador não levantado e o outro for um operador levantado, o não levantado é melhor.
  • Se nenhum membro da função foi encontrado para ser melhor, e todos os parâmetros de Mᵥ têm um argumento correspondente, enquanto os argumentos padrão precisam ser substituídos por pelo menos um parâmetro opcional em Mₓ, então Mᵥ é melhor do que Mₓ.
  • Se para pelo menos um parâmetro Mᵥ usar a melhor opção de passagem de parâmetros (§12.6.4.4) do que o parâmetro correspondente em Mₓ e nenhum dos parâmetros em Mₓ usar a melhor opção de passagem de parâmetros do que Mᵥ, Mᵥ é melhor do que Mₓ.
  • Caso contrário, se ambos os métodos tiverem coleções de parâmetros e forem aplicáveis apenas em suas formas expandidas, então Mᵢ é melhor do que Mₑ se o mesmo conjunto de argumentos corresponder aos elementos de coleção para ambos os métodos, e um dos seguintes mantém (isso corresponde a https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/collection-expressions-better-conversion.md):
    • ambas as coleções de parâmetros não são do tipo span_type, e existe uma conversão implícita de coleções de parâmetros de Mᵢ para coleções de parâmetros de Mₑ
    • coleção de parâmetros de Mᵢ é System.ReadOnlySpan<Eᵢ>, e a coleção de parâmetros de Mₑ é System.Span<Eₑ>, e existe uma conversão de identidade de Eᵢ para Eₑ
    • coleção de parâmetros de Mᵢ é System.ReadOnlySpan<Eᵢ> ou System.Span<Eᵢ>, e a coleção de parâmetros de Mₑ é uma array_or_array_interface__type com tipo de elemento Eₑ, e existe uma conversão de identidade de Eᵢ para Eₑ
  • Caso contrário, nenhum membro da função é superior.

A razão pela qual a nova regra de desempate é colocada no final da lista é o último subitem

  • ambas as coleções de parâmetros não são do tipo span_type, e existe uma conversão implícita de coleções de parâmetros de Mᵢ para coleções de parâmetros de Mₑ

é aplicável a matrizes e, portanto, executar o tie-break mais cedo introduzirá uma alteração de comportamento para cenários existentes.

Por exemplo:

class Program
{
    static void Main()
    {
        Test(1);
    }

    static void Test(in int x, params C2[] y) {} // There is an implicit conversion from `C2[]` to `C1[]`
    static void Test(int x, params C1[] y) {} // Better candidate because of "better parameter-passing choice"
}

class C1 {}
class C2 : C1 {}

Se qualquer uma das regras de desempate anteriores se aplicar (incluindo a regra "melhores conversões de argumentos"), o resultado da resolução de sobrecarga pode ser diferente em comparação com o caso em que uma expressão de coleção explícita é usada como argumento.

Por exemplo:

class Program
{
    static void Test1()
    {
        M1(['1', '2', '3']); // IEnumerable<char> overload is used because `char` is an exact match
        M1('1', '2', '3');   // IEnumerable<char> overload is used because `char` is an exact match
    }

    static void M1(params IEnumerable<char> value) {}
    static void M1(params System.ReadOnlySpan<MyChar> value) {}

    class MyChar
    {
        private readonly int _i;
        public MyChar(int i) { _i = i; }
        public static implicit operator MyChar(int i) => new MyChar(i);
        public static implicit operator char(MyChar c) => (char)c._i;
    }

    static void Test2()
    {
        M2([1]); // Span overload is used
        M2(1);   // Array overload is used, not generic
    }

    static void M2<T>(params System.Span<T> y){}
    static void M2(params int[] y){}

    static void Test3()
    {
        M3("3", ["4"]); // Ambiguity, better-ness of argument conversions goes in opposite directions.
        M3("3", "4");   // Ambiguity, better-ness of argument conversions goes in opposite directions.
                        // Since parameter types are different ("object, string" vs. "string, object"), tie-breaking rules do not apply
    }

    static void M3(object x, params string[] y) {}
    static void M3(string x, params Span<object> y) {}
}

No entanto, a nossa principal preocupação são os cenários em que as sobrecargas diferem apenas pelo tipo de coleção de params, mas esses tipos de coleção têm o mesmo tipo de elemento. O comportamento deve ser consistente com as expressões explícitas de recolha para esses casos.

A condição "se o mesmo conjunto de argumentos corresponder aos elementos de coleção para ambos os métodos" é importante para cenários como:

class Program
{
    static void Main()
    {
        Test(x: 1, y: 2); // Ambiguous
    }

    static void Test(int x, params System.ReadOnlySpan<int> y) {}
    static void Test(int y, params System.Span<int> x) {}
}

Não parece razoável "comparar" coleções que são construídas a partir de elementos diferentes.

Esta seção foi revisada em LDM e foi aprovada.

Um efeito dessas regras é que, quando params de diferentes tipos de elementos são expostos, eles serão ambíguos quando chamados com uma lista de argumentos vazia. Por exemplo:

class Program
{
    static void Main()
    {
        // Old scenarios
        C.M1(); // Ambiguous since params arrays were introduced
        C.M1([]); // Ambiguous since params arrays were introduced

        // New scenarios
        C.M2(); // Ambiguous in C# 13
        C.M2([]); // Ambiguous in C# 13
        C.M3(); // Ambiguous in C# 13
        C.M3([]); // Ambiguous in C# 13
    }

    public static void M1(params int[] a) {
    }
    
    public static void M1(params int?[] a) {
    }
    
    public static void M2(params ReadOnlySpan<int> a) {
    }
    
    public static void M2(params Span<int?> a) {
    }
    
    public static void M3(params ReadOnlySpan<int> a) {
    }
    
    public static void M3(params ReadOnlySpan<int?> a) {
    }
}

Dado que priorizamos o tipo de elemento acima de tudo, isso parece razoável; Não há nada para dizer ao idioma se o usuário preferiria int? em vez de int neste cenário.

Vinculação dinâmica

Formas expandidas de candidatos que utilizam coleções de parâmetros não-array não serão consideradas como candidatos válidos pelo associador de tempo de execução do C# atual.

Se o primary_expression não tiver o tipo de tempo de compilação dynamic, a invocação do método passa por uma verificação limitada em tempo de compilação, conforme descrito no §12.6.5 Verificação em tempo de compilação da invocação de membro dinâmico.

Se apenas um candidato for aprovado no teste, a convocação do candidato é vinculada estaticamente quando estiverem preenchidas todas as seguintes condições:

  • o candidato é uma função local
  • ou o candidato não é genérico, ou os seus argumentos de tipo são explicitamente especificados;
  • Não existe ambiguidade entre a forma normal e a forma expandida do candidato que não possa ser resolvida em tempo de compilação.

Caso contrário, o invocation_expression é vinculado dinamicamente.

Se apenas um candidato for aprovado no teste acima:

  • se esse candidato for uma função local, ocorre um erro em tempo de compilação;
  • Se esse candidato for aplicável somente na forma expandida utilizando coleções de parâmetros que não sejam de matriz, ocorrerá um erro em tempo de compilação.

Também devemos considerar reverter/corrigir a violação de especificações que afeta atualmente as funções locais, consulte https://github.com/dotnet/roslyn/issues/71399.

LDM confirmou que queremos corrigir esta violação de especificação.

Árvores de expressão

Não há suporte para expressões de coleção em árvores de expressão. Da mesma forma, formas expandidas de coleções de parâmetros que não são arrays não serão suportadas em árvores de expressão. Não mudaremos a forma como o compilador vincula lambdas para árvores de expressão com o objetivo de evitar o uso de APIs utilizando formas expandidas de coleções de params não array.

Ordem de avaliação com coleções não baseadas em arrays em cenários não triviais

Esta seção foi revisada em LDM e foi aprovada. Apesar do fato de que os casos de matriz se desviam de outras coleções, a especificação de idioma oficial não precisa especificar regras diferentes para matrizes. Os desvios poderiam simplesmente ser tratados como um artefato de implementação. Ao mesmo tempo, não pretendemos alterar o comportamento existente em torno de matrizes.

Argumentos nomeados

Uma instância de coleção é criada e preenchida depois que o argumento lexicamente anterior é avaliado, mas antes que o argumento lexicamente seguinte seja avaliado.

Por exemplo:

class Program
{
    static void Main()
    {
        Test(b: GetB(), c: GetC(), a: GetA());
    }

    static void Test(int a, int b, params MyCollection c) {}

    static int GetA() => 0;
    static int GetB() => 0;
    static int GetC() => 0;
}

A ordem de avaliação é a seguinte:

  1. GetB chama-se
  2. MyCollection é criado e preenchido, GetC é chamado no processo
  3. GetA chama-se
  4. Test chama-se

Observe que, no caso da matriz params, a matriz é criada antes do método de destino ser invocado, depois que todos os argumentos são avaliados em sua ordem lexical.

Atribuição composta

Uma instância de coleção é criada e preenchida depois que o índice lexicamente anterior é avaliado, mas antes que o índice lexicamente seguinte seja avaliado. A instância é usada para invocar getter e setter do indexador de destino.

Por exemplo:

class Program
{
    static void Test(Program p)
    {
        p[GetA(), GetC()]++;
    }

    int this[int a, params MyCollection c] { get => 0; set {} }

    static int GetA() => 0;
    static int GetC() => 0;
}

A ordem de avaliação é a seguinte:

  1. GetA é chamado e armazenado em cache
  2. MyCollection é criado, preenchido e armazenado em cache, GetC é chamado no processo
  3. O getter do indexador é invocado com valores armazenados em cache para índices
  4. O resultado é incrementado
  5. O setter do indexador é invocado com valores de índices armazenados em cache e o resultado do incremento.

Um exemplo com uma coleção vazia:

class Program
{
    static void Test(Program p)
    {
        p[GetA()]++;
    }

    int this[int a, params MyCollection c] { get => 0; set {} }

    static int GetA() => 0;
}

A ordem de avaliação é a seguinte:

  1. GetA é chamado e armazenado em cache
  2. Um MyCollection vazio é criado e armazenado em cache
  3. O getter do indexador é invocado com valores armazenados em cache para índices
  4. O resultado é incrementado
  5. O setter do indexador é invocado com valores de índices armazenados em cache e o resultado do incremento.

Inicializador de objetos

Uma instância de coleção é criada e preenchida depois que o índice lexicamente anterior é avaliado, mas antes que o índice lexicamente seguinte seja avaliado. A instância é usada para invocar o getter do indexador tantas vezes quanto necessário.

Por exemplo:

class C1
{
    public int F1;
    public int F2;
}

class Program
{
    static void Test()
    {
        _ = new Program() { [GetA(), GetC()] = { F1 = GetF1(), F2 = GetF2() } };
    }

    C1 this[int a, params MyCollection c] => new C1();

    static int GetA() => 0;
    static int GetC() => 0;
    static int GetF1() => 0;
    static int GetF2() => 0;
}

A ordem de avaliação é a seguinte:

  1. GetA é chamado e armazenado em cache
  2. MyCollection é criado, preenchido e armazenado em cache, GetC é chamado no processo
  3. O getter do indexador é invocado com valores armazenados em cache para índices
  4. GetF1 é avaliado e atribuído a F1 campo de C1 reajustado na etapa anterior
  5. O getter do indexador é invocado com valores armazenados em cache para índices
  6. GetF2 é avaliado e atribuído a F2 campo de C1 reajustado na etapa anterior

Observe que, no caso de matriz params, seus elementos são avaliados e armazenados em cache, mas uma nova instância de uma matriz (com os mesmos valores dentro) é usada para cada invocação do getter do indexador. Para o exemplo acima, a ordem de avaliação é a seguinte:

  1. GetA é chamado e armazenado em cache
  2. GetC é chamado e armazenado em cache
  3. O getter do indexador é invocado com o GetA em cache, e uma nova matriz é preenchida com o GetC em cache.
  4. GetF1 é avaliado e atribuído a F1 campo de C1 reajustado na etapa anterior
  5. O getter do indexador é invocado com o GetA em cache, e uma nova matriz é preenchida com o GetC em cache.
  6. GetF2 é avaliado e atribuído a F2 campo de C1 reajustado na etapa anterior

Um exemplo com uma coleção vazia:

class C1
{
    public int F1;
    public int F2;
}

class Program
{
    static void Test()
    {
        _ = new Program() { [GetA()] = { F1 = GetF1(), F2 = GetF2() } };
    }

    C1 this[int a, params MyCollection c] => new C1();

    static int GetA() => 0;
    static int GetF1() => 0;
    static int GetF2() => 0;
}

A ordem de avaliação é a seguinte:

  1. GetA é chamado e armazenado em cache
  2. Um MyCollection vazio é criado e armazenado em cache
  3. O getter do indexador é invocado com valores armazenados em cache para índices
  4. GetF1 é avaliado e atribuído a F1 campo de C1 reajustado na etapa anterior
  5. O getter do indexador é invocado com valores armazenados em cache para índices
  6. GetF2 é avaliado e atribuído a F2 campo de C1 reajustado na etapa anterior

Ref. segurança

A seção de segurança ref collection expressions é aplicável à construção de coleções de parâmetros quando as APIs são invocadas em sua forma expandida.

Os parâmetros Params são implicitamente scoped quando o seu tipo é uma estrutura de referência (ref struct). UnscopedRefAttribute pode ser usado para substituir isso.

Metadados

Nos metadados, podemos marcar parâmetros params não-array com System.ParamArrayAttribute, da mesma forma que são marcadas as matrizes params hoje. No entanto, parece que será mais seguro usarmos um atributo diferente para parâmetros params que não sejam arrays. Por exemplo, o compilador VB atual não será capaz de consumi-los decorados com ParamArrayAttribute nem na forma normal, nem na forma expandida. Portanto, a adição de um modificador 'params' é provável que cause problemas para os consumidores de VB e, muito provavelmente, para consumidores de outras linguagens ou ferramentas.

Sendo assim, parâmetros não matriz params são marcados com um novo System.Runtime.CompilerServices.ParamCollectionAttribute.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
    public sealed class ParamCollectionAttribute : Attribute
    {
        public ParamCollectionAttribute() { }
    }
}

Esta seção foi revisada em LDM e foi aprovada.

Perguntas abertas

Alocações de pilha

Aqui está uma citação de https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#unresolved-questions: "Alocações de pilha para coleções enormes podem saturar a pilha. Deve o compilador ter uma heurística para colocar esses dados no monte? A linguagem não deve ser especificada para permitir essa flexibilidade? Devemos seguir as especificações para params Span<T>." Parece que temos de responder às perguntas no contexto desta proposta.

[Resolvido] scoped parâmetros implícitos

Houve uma sugestão de que, quando params modifica um parâmetro ref struct, ele deve ser considerado como declarado scoped. Argumenta-se que o número de casos em que se deseja que o parâmetro esteja delimitado é de praticamente 100% ao examinar os casos na BCL. Em alguns casos que precisam disso, a definição padrão pode ser substituída por [UnscopedRef].

No entanto, pode ser indesejável alterar o padrão simplesmente com base na presença do modificador params. Especialmente em cenários de substituições/implementos, o modificador params não precisa corresponder.

Resolução:

Parâmetros são implicitamente delimitados no âmbito - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-11-15.md#params-improvements.

[Resolvido] Considere aplicar scoped ou params em substituições

Já afirmamos anteriormente que params parâmetros devem ser scoped por padrão. No entanto, isso introduz um comportamento estranho ao substituir, devido às nossas regras existentes acerca de reiterar params:

class Base
{
    internal virtual Span<int> M1(scoped Span<int> s1, params Span<int> s2) => throw null!;
}

class Derived : Base
{
    internal override Span<int> M1(Span<int> s1, // Error, missing `scoped` on override
                                   Span<int> s2  // Proposal: Error: parameter must include either `params` or `scoped`
                                  ) => throw null!;
}

Temos uma diferença de comportamento entre transportar o params e transportar o scoped através de substituições aqui: params é herdado implicitamente, e com ele scoped, enquanto scoped por si só não é herdado implicitamente e deve ser repetido em todos os níveis.

Proposta: Devemos impor que as substituições de parâmetros params devem indicar explicitamente params ou scoped se a definição original for um parâmetro scoped. Em outras palavras, s2 em Derived deve ter params, scopedou ambos.

Resolução:

Exigiremos declarar explicitamente scoped ou params na substituição de um parâmetro params quando um parâmetro nãoparams for necessário para fazê-lo - https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-21.md#params-and-scoped-across-overrides.

[Resolvido] A presença de membros obrigatórios deve impedir a declaração do parâmetro params?

Considere o seguinte exemplo:

using System.Collections;
using System.Collections.Generic;

public class MyCollection1 : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(long l) => throw null;

    public required int F; // Collection has required member and constructor doesn't initialize it explicitly
}

class Program
{
    static void Main()
    {
        Test(2, 3); // error CS9035: Required member 'MyCollection1.F' must be set in the object initializer or attribute constructor.
    }

    // Proposal: An error is reported for the parameter indicating that the constructor that is required
    // to be available doesn't initialize required members. In other words, one is able
    // to declare such a parameter under the specified conditions.
    static void Test(params MyCollection1 a)
    {
    }
}

Resolução:

Vamos validar os membros required em função do construtor que é usado para determinar a elegibilidade para ser um parâmetro params no local da declaração - https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-21.md#required-members-and-params-parameters.

Alternativas

Existe uma proposta alternativa que se estende params apenas para ReadOnlySpan<T>.

Além disso, pode-se dizer que, com as expressões de coleção agora na língua, não há necessidade de estender o suporte de params. Para qualquer tipo de coleção. Para consumir uma API com tipo de coleção, um desenvolvedor só precisa adicionar dois caracteres, [ antes da lista expandida de argumentos e ] depois dela. Tendo isso em conta, estender o suporte a params pode ser um exagero, especialmente porque é improvável que outras linguagens suportem tão cedo o consumo de parâmetros params que não sejam matrizes.