Compartilhar via


params Collections

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação 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 idioma (LDM).

Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .

Problema do especialista: https://github.com/dotnet/csharplang/issues/7700

Resumo

Na linguagem C# 12, foi adicionado suporte para criar instâncias de tipos de coleção além de apenas matrizes. Confira expressões de coleção. Esta proposta estende o suporte 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 arbitrária de argumentos. Hoje o parâmetro params deve ser do tipo array. No entanto, pode ser benéfico para um desenvolvedor poder ter a mesma conveniência ao chamar APIs que aceitam 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 utiliza um tipo de coleção, os desenvolvedores geralmente adicionam uma sobrecarga params que usa uma matriz, constroem a coleção de destino e chamam a sobrecarga original com essa coleção. Com isso, os consumidores da API precisam negociar uma alocação de uma matriz extra para conveniência.

Outra motivação é a capacidade de adicionar uma sobrecarga de intervalo de parâmetros e fazê-la ter precedência sobre a versão da matriz, apenas recompilando o código-fonte existente.

Design detalhado

Parâmetros de método

A seção parâmetros do Método é ajustada da seguinte maneira.

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 e um identificador de . Uma coleção de parâmetros declara um único parâmetro do tipo fornecido 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 (consulte https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#conversions):

  • Um tipo de matriz unidimensionalT[], nesse caso, o tipo de elemento é T
  • Um tipo de intervalo.
    • System.Span<T>
    • System.ReadOnlySpan<T>
      em quais casos o tipo de elemento é T
  • Um tipo com um método de criação apropriado que seja pelo menos tão acessível quando 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 em que:
    • 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 em que:

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

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

  • Um 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 quais 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 fornecido seja especificado ou permite que zero ou mais argumentos do tipo de elemento da coleção sejam especificados. Coleções de parâmetros são descritas ainda mais em coleções de parâmetros .

Um 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 na criação de uma coleção vazia.

Coleções de parâmetros

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

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

Observação: não é possível combinar o modificador de params com os modificadores in, outou ref. nota final

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

  • O argumento fornecido 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. Nesse 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, em que 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 usados 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. Quando a instância de coleção é construída, os argumentos não convertidos originais são usados.

Exceto para 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 estará disponível somente se a forma normal do método não for aplicável e somente se um método aplicável com a mesma assinatura do formulário expandido ainda não estiver declarado no mesmo tipo.

Uma ambiguidade potencial surge entre a forma normal e a forma expandida do método com uma única coleção de parâmetros como argumento quando ela pode ser usada tanto como a própria coleção de parâmetros quanto como elemento da coleção de parâmetros ao mesmo tempo. No entanto, a ambiguidade não apresenta nenhum problema, pois pode ser resolvida inserindo uma conversão ou usando uma expressão de coleção, se necessário.

Assinaturas e sobrecarga

Todas as regras relativas ao modificador params em assinaturas e sobrecargas permanecem como estão.

Membro da função aplicável

A seção Membro de função Applicable é ajustada da seguinte maneira.

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 de 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 para as versões de idioma C# 12 e inferiores.
  • O formulário expandido é construído substituindo a coleção de parâmetros na declaração do membro da função por parâmetros de valor zero ou superior do tipo de elemento da coleção de parâmetros de modo que o número de argumentos na lista de argumentos A corresponde 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 poderá ser construída e, portanto, não será aplicável.
  • Caso contrário, o formulário expandido é aplicável se, para cada argumento em A, um dos seguintes for verdadeiro:
    • o modo de passagem de parâmetro do argumento é idêntico ao modo de passagem de parâmetro 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âmetro do argumento é o valor e o modo de passagem de parâmetro do parâmetro correspondente é a entrada, e existe uma conversão implícita da expressão de argumento para o tipo do parâmetro correspondente

Membro de função Better

A seção Membro de função Better é ajustada da seguinte maneira.

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âmetro {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, em ordem, para determinar o membro de função melhor.

  • Se Mᵢ for um método não genérico e Mₑ for um método genérico, Mᵢ será melhor que Mₑ.
  • Caso contrário, se Mᵢ for aplicável em sua forma normal e Mₑ tiver uma coleção de parâmetros e for aplicável somente em sua forma expandida, Mᵢ será melhor que Mₑ.
  • Caso contrário, se ambos os métodos tiverem coleções de parâmetros e forem aplicáveis somente em suas formas expandidas e se a coleção de parâmetros de Mᵢ tiver menos elementos do que a coleção de params de Mₑ, Mᵢ será melhor que Mₑ.
  • Caso contrário, se Mᵥ tiver tipos de parâmetro mais específicos do que Mₓ, Mᵥ será melhor que Mₓ. 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ₓ. Os tipos de parâmetros de Mᵥsão mais específicos do que os de Mₓse, para cada parâmetro, Rx não for menos específico que Sx, e, para pelo menos um parâmetro, Rx for mais específico do que Sx:
    • Um parâmetro de tipo é menos específico do que um parâmetro não tipo.
    • 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 for mais específico e nenhum argumento de tipo for 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 elevado e o outro for um operador elevado, o não elevado será melhor.
  • Se nenhum membro da função tiver sido considerado melhor e todos os parâmetros de Mᵥ tiverem um argumento correspondente, enquanto os argumentos padrão precisarão ser substituídos por pelo menos um parâmetro opcional em Mₓ, então Mᵥ será melhor do que Mₓ.
  • Se, por 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ₓ usa a melhor opção de passagem de parâmetro 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 somente em seus formulários expandidos, Mᵢ será melhor do que Mₑ se o mesmo conjunto de argumentos corresponder aos elementos de coleção de ambos os métodos e uma das seguintes retenções (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 span_types, e existe uma conversão implícita da coleção de parâmetros de Mᵢ para a coleção de parâmetros de Mₑ
    • a 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ₑ é um array_or_array_interface__type com o tipo de elemento Eₑ, e existe uma conversão de identidade de Eᵢ para Eₑ
  • Caso contrário, nenhum membro de função é melhor.

O motivo pelo qual a nova regra de desempate é colocada no final da lista é devido ao último subitem.

  • Ambas as coleções de parâmetros não são span_types, e existe uma conversão implícita da coleção de parâmetros de Mᵢ para a coleção de parâmetros de Mₑ

é aplicável a matrizes e, portanto, realizar o desempate antecipadamente introduzirá uma alteração de comportamento dos 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 {}

Caso alguma das regras anteriores de desempate seja aplicável (incluindo a regra de "conversões de argumentos melhores"), o resultado da resolução de sobrecargas 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, nossa principal preocupação são cenários em que as sobrecargas diferem apenas pelo tipo de coleção de parâmetros, mas os tipos de coleção têm o mesmo tipo de elemento. O comportamento deve ser consistente com expressões de coleção explícitas para esses casos.

A condição "caso o mesmo conjunto de argumentos corresponda aos elementos da 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 criadas com base em elementos diferentes.

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

Um efeito dessas regras é que, quando params de diferentes tipos de elementos forem expostos, elas serão ambíguas quando chamadas 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 em todo o resto, isso parece razoável; não há nada para dizer ao idioma se o usuário prefere int? em vez de int neste cenário.

Vinculação Dinâmica

As formas expandidas de candidatos que utilizam coleções de parâmetros que não são matrizes não serão consideradas como candidatas válidas pelo associador de tempo de execução do C# atual.

Se a primary_expression não tiver o tipo dynamicno tempo de compilação, a invocação do método passará por uma verificação de tempo de compilação limitada, conforme descrito na seção §12.6.5 Verificação de tempo de compilação da invocação de membro dinâmico.

Se apenas um único candidato passar no teste, a invocação do candidato será vinculada estaticamente quando todas as seguintes condições forem atendidas:

  • o candidato é uma função local
  • o candidato não é genérico ou seus argumentos de tipo são explicitamente especificados;
  • não há ambiguidade entre formas normais e expandidas do candidato que não podem ser resolvidas no tempo de compilação.

Caso contrário, o invocation_expression é associado dinamicamente.

Se apenas um único candidato passou no teste acima:

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

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

LDM confirmou que queremos corrigir essa 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, não haverá suporte para formas expandidas de coleções de parâmetros que não são matrizes em árvores de expressão. Não vamos alterar a forma como o compilador associa 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 matrizes.

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

Esta seção foi revisada no LDM e foi aprovada. Apesar de os casos de matriz se desviarem de outras coleções, a especificação oficial do idioma não precisa especificar regras diferentes para matrizes. Os desvios podem ser simplesmente 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 populada 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 é chamado
  2. MyCollection é criado e preenchido, GetC é chamado durante o processo
  3. GetA é chamado
  4. Test é chamado

Observe que, no caso da matriz params, a matriz é criada logo 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 populada depois que o índice lexicamente anterior é avaliado, mas antes que o índice lexicamente a seguir 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 armazenados em cache para índices 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 armazenados em cache para índices e o resultado do incremento

Inicializador de objeto

Uma instância de coleção é criada e populada depois que o índice lexicamente anterior é avaliado, mas antes que o índice lexicamente a seguir seja avaliado. A instância é usada para invocar o getter do indexador quantas vezes forem necessárias, quando 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 ao campo F1 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 ao campo F2 de C1 reajustado na etapa anterior

Observe que, no caso da matriz de parâmetros, 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 GetA armazenado em cache e uma nova matriz preenchida com GetC armazenado em cache
  4. GetF1 é avaliado e atribuído ao campo F1 de C1 reajustado na etapa anterior
  5. O getter do indexador é invocado com GetA armazenado em cache e uma nova matriz preenchida com GetC armazenado em cache
  6. GetF2 é avaliado e atribuído ao campo F2 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 método getter do indexador é invocado com valores em cache para índices
  4. GetF1 é avaliado e atribuído ao campo F1 de C1 reajustado na etapa anterior
  5. O método getter do indexador é chamado com valores de índice armazenados em cache.
  6. GetF2 é avaliado e atribuído ao campo F2 de C1 reajustado na etapa anterior

Segurança de ref

A seção de segurança de referência de expressões de coleção é 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 seu tipo é um ref struct. UnscopedRefAttribute pode ser usado para substituir isso.

Metadados

Em metadados, poderíamos marcar parâmetros que não são matrizes de params com System.ParamArrayAttribute, assim como as matrizes de params são marcadas hoje. No entanto, parece que será muito mais seguro usar um atributo diferente para parâmetros de params não matriz. Por exemplo, o compilador de VB atual não poderá consumi-los decorados com ParamArrayAttribute nem no normal nem na forma expandida. Portanto, é provável que uma adição do modificador 'params' interrompa os consumidores de VB, e muito provavelmente os consumidores de outras linguagens ou ferramentas.

Considerando isso, os parâmetros params que não são de matriz 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 no LDM e foi aprovada.

Perguntas abertas

Alocações de pilha

Confira 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 grandes coleções podem explodir a pilha. O compilador deve ter uma heurística para colocar esses dados no heap? O idioma não deve ser especificado para permitir essa flexibilidade? Devemos seguir a especificação de params Span<T>." Parece que temos que responder às perguntas considerando o contexto desta proposta.

[Resolvido] Parâmetros scoped implicitamente

Houve uma sugestão de que, quando params modifica um parâmetro ref struct, ele deve ser considerado como declarado scoped. O argumento faz com que o número de casos que deverão ter o parâmetro no escopo é praticamente total quando os casos do BCL são analisados. Em alguns casos que precisam disso, o padrão pode ser substituído 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/implementações, o modificador params não precisa ser correspondente.

Resolução:

Os parâmetros params têm escopo implícito – https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-11-15.md#params-improvements.

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

Já afirmamos anteriormente que os parâmetros de params devem ser scoped por padrão. No entanto, isso introduz um comportamento estranho na substituição devido às nossas regras existentes sobre a retomada de 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 carregar o params e carregar o scoped nas 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 declarar 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 explicitamente a declaração scoped ou params na substituição de um parâmetro params, quando seria necessário um parâmetro que não sejaparams para fazer isso - 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:

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

Alternativas

Há uma proposta de alternativa que estende params apenas para ReadOnlySpan<T>.

Além disso, pode-se dizer que, com expressões de coleção agora no idioma, não há necessidade de estender o suporte params. Para qualquer tipo de coleção. Para consumir uma API com o tipo de coleção, um desenvolvedor simplesmente precisa adicionar dois caracteres, [ antes da lista expandida de argumentos e ] depois dela. Considerando isso, estender o suporte a params pode ser um exagero, especialmente já que é improvável que outros idiomas passem a oferecer suporte ao consumo de parâmetros que não são de matriz de params no curto prazo.