Interfaces de Estruturas de Referência

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 campeã: https://github.com/dotnet/csharplang/issues/7608

Resumo

Esta proposta expandirá as capacidades dos ref struct de modo a que possam implementar interfaces e participar como argumentos de tipo genéricos.

Motivação

A incapacidade de ref struct implementar interfaces significa que eles não podem participar de técnicas de abstração bastante fundamentais do .NET. Um Span<T>, mesmo que tenha todos os atributos de uma lista sequencial não pode participar de métodos que tomam IReadOnlyList<T>, IEnumerable<T>, etc ... Em vez disso, métodos específicos devem ser codificados para Span<T> que tenham praticamente a mesma implementação. Permitir que ref struct implemente interfaces permitirá que as operações sejam abstraídas sobre eles como ocorre com outros tipos.

Projeto de Execução

struct ref de interfaces

A linguagem permitirá que tipos ref struct implementem interfaces. A sintaxe e as regras são as mesmas que para normais struct, com algumas exceções para considerar as limitações de tipos ref struct.

A capacidade de implementar interfaces não afeta as limitações existentes em instâncias de ref struct de boxe. Isso significa que, mesmo que um ref struct implemente uma interface específica, ele não pode ser diretamente convertido para ele, pois isso representa uma ação de boxe.

ref struct File : IDisposable
{
    private SafeHandle _handle;
    public void Dispose()
    {
        _handle.Dispose();
    }
}

File f = ...;
// Error: cannot box `ref struct` type `File`
IDisposable d = f;

A capacidade de implementar interfaces só é útil quando combinada com a capacidade de ref struct participarem em argumentos genéricos (como exposto mais adiante).

Para permitir que as interfaces cubram toda a expressividade de um ref struct e os problemas de ciclo de vida que ele pode apresentar, a linguagem permitirá que [UnscopedRef] apareça nos métodos e propriedades da interface. Isso é necessário, pois permite que interfaces que abstraem sobre struct tenham a mesma flexibilidade que usar um struct diretamente. Considere o seguinte exemplo:

interface I1
{
    [UnscopedRef]
    ref int P1 { get; }
    ref int P2 { get; }
}

struct S1
{
    [UnscopedRef]
    internal ref int P1 { get {...} }

    internal ref int P2 { get {...} }
}

ref int M<T>(T t, S1 s)
    where T : I1, allows ref struct
{
    // Error: may return ref to t
    return ref t.P1;

    // Error: may return ref to t
    return ref s.P1;

    // Okay
    return ref t.P2;

    // Okay
    return ref s.P2;
}

Quando um membro struct / ref struct implementa um membro da interface com um atributo [UnscopedRef], o membro implementador também pode ser decorado com [UnscopedRef] mas não é necessário. No entanto, um membro com [UnscopedRef] não pode ser usado para implementar um membro que não possui o atributo (detalhes).

interface I1
{
    [UnscopedRef]
    ref int P1 { get; }
    ref int P2 { get; }
}

struct S1
{
    internal ref int P1 { get {...} }
    internal ref int P2 { get {...} }
}

struct S2
{
    [UnscopedRef]
    internal ref int P1 { get {...} }
    internal ref int P2 { get {...} }
}

struct S3 : I1
{
    internal ref int P1 { get {...} }
    // Error: P2 is marked with [UnscopedRef] and cannot implement I1.P2 as is not marked 
    // with [UnscopedRef]
    [UnscopedRef]
    internal ref int P2 { get {...} }
}

class C1 : I1
{
    internal ref int P1 { get {...} }
    internal ref int P2 { get {...} }
}

Os métodos de interface padrão representam um problema para ref struct pois não há proteções contra a implementação padrão que encaixota o membro this.

interface I1
{
    void M()
    {
        // Danger: both of these box if I1 is implemented by a ref struct
        I1 local1 = this;
        object local2 = this;
    }
}

// Error: I1.M cannot implement interface member I1.M() for ref struct S
ref struct S : I1 { }

Para lidar com isso, um ref struct será forçado a implementar todos os membros de uma interface, mesmo que eles tenham implementações padrão. O tempo de execução também será atualizado para lançar uma exceção se um membro de uma interface padrão for chamado num tipo de ref struct.

Para evitar uma exceção em tempo de execução, o compilador relatará um erro para uma invocação de um método (ou propriedade) de instância não virtual em um parâmetro de tipo que permite uma estrutura de referência. Aqui está um exemplo:

public interface I1
{
    sealed void M3() {}
}

class C
{
    static void Test2<T>(T x) where T : I1, allows ref struct
    {
#line 100
        x.M3(); // (100,9): error: A non-virtual instance interface member cannot be accessed on a type parameter that allows ref struct.
    }
}

Há também uma questão de design em aberto sobre como relatar um aviso para uma invocação de um método (ou propriedade) de instância virtual (não abstrata) em um parâmetro de tipo que permite ref struct.

Notas detalhadas:

  • Um ref struct pode implementar uma interface
  • Um ref struct não pode ser parte de membros padrão de uma interface
  • Um ref struct não pode ser convertido em interfaces que implementa, pois é uma operação de boxe

ref struct Parâmetros Genéricos

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : restrictive_type_parameter_constraints
    | allows_type_parameter_constraints_clause
    | restrictive_type_parameter_constraints ',' allows_type_parameter_constraints_clause

restrictive_type_parameter_constraints
    : primary_constraint
    | secondary_constraints
    | constructor_constraint
    | primary_constraint ',' secondary_constraints
    | primary_constraint ',' constructor_constraint
    | secondary_constraints ',' constructor_constraint
    | primary_constraint ',' secondary_constraints ',' constructor_constraint
    ;

primary_constraint
    : class_type
    | 'class'
    | 'struct'
    | 'unmanaged'
    ;

secondary_constraints
    : interface_type
    | type_parameter
    | secondary_constraints ',' interface_type
    | secondary_constraints ',' type_parameter
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

allows_type_parameter_constraints_clause
    : 'allows' allows_type_parameter_constraints

allows_type_parameter_constraints
    : allows_type_parameter_constraint
    | allows_type_parameter_constraints ',' allows_type_parameter_constraint

allows_type_parameter_constraint
    : ref_struct_clause

ref_struct_clause
    : 'ref' 'struct'

A linguagem permitirá que parâmetros genéricos optem por apoiar ref struct como argumentos usando a sintaxe allows ref struct dentro de uma cláusula where:

T Identity<T>(T p)
    where T : allows ref struct
    => p;

// Okay
Span<int> local = Identity(new Span<int>(new int[10]));

Isso é semelhante a outros itens em uma cláusula where na medida em que especifica os recursos do parâmetro genérico. A diferença é que outros itens de sintaxe limitam o conjunto de tipos que podem cumprir um parâmetro genérico enquanto allows ref struct expande o conjunto de tipos. Isso é efetivamente uma anti-restrição, pois remove a restrição implícita que ref struct não pode satisfazer um parâmetro genérico. Como tal, é dado um novo prefixo de sintaxe, allows, para tornar isso mais claro.

Um parâmetro de tipo vinculado por allows ref struct tem todos os comportamentos de um tipo ref struct:

  1. Instâncias dele não podem ser encaixotadas
  2. As instâncias participam em regras de tempo de vida como um ref struct normal
  3. O parâmetro tipo não pode ser usado em campos static, elementos de uma matriz, etc.
  4. As instâncias podem ser marcadas com scoped

Exemplos destas regras em ação:

interface I1 { }
I1 M1<T>(T p)
    where T : I1, allows ref struct
{
    // Error: cannot box potential ref struct
    return p;
}

T M2<T>(T p)
    where T : allows ref struct
{
    Span<int> span = stackalloc int[42];

    // The safe-to-escape of the return is current method because one of the inputs is
    // current method
    T t = M3<int, T>(span);

    // Error: the safe-to-escape is current method.
    return t;

    // Okay
    return default;
    return p;
}

R M3<T, R>(Span<T> span)
    where R : allows ref struct
{
    return default;
}

A anti-restrição não é "herdada" de uma restrição de tipo de parâmetro. Por exemplo, S no código abaixo não pode ser substituído por uma ref struct:

class C<T, S>
    where T : allows ref struct
    where S : T
{}

Notas detalhadas:

  • Um parâmetro genérico where T : allows ref struct não pode
    • Ter where T : U onde U é um tipo de referência conhecido
    • Restrição where T : class presente
    • Não pode ser usado como um argumento genérico, a menos que o parâmetro correspondente também seja where T: allows ref struct
  • O allows ref struct deve ser a última restrição na cláusula where
  • Um parâmetro de tipo T que tem allows ref struct tem as mesmas limitações que um tipo ref struct.

Representação em metadados

Os parâmetros de tipo que permitem structs de referência serão codificados em metadados conforme descrito no documento de genéricos semelhantes a byref . Especificamente, usando os valores dos sinalizadores CorGenericParamAttr.gpAllowByRefLike(0x0020) ou System.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020). Pode-se determinar se o tempo de execução suporta o recurso, verificando a presença do campo System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics. As APIs foram adicionadas em https://github.com/dotnet/runtime/pull/98070.

Declaração de using

Uma instrução using reconhecerá e usará a implementação da interface IDisposable quando o recurso for uma ref struct.

ref struct S2 : System.IDisposable
{
    void System.IDisposable.Dispose()
    {
    }
}

class C
{
    static void Main()
    {
        using (new S2())
        {
        } // S2.System.IDisposable.Dispose is called
    }
}

Observe que é dada preferência a um método Dispose que implementa o padrão e somente se um não for encontrado, IDisposable implementação é usada.

Uma instrução using reconhecerá e usará a implementação da interface IDisposable quando o recurso for um parâmetro de tipo se allows ref struct e IDisposable estão no seu conjunto de interfaces efetivas.

class C
{
    static void Test<T>(T t) where T : System.IDisposable, allows ref struct
    {
        using (t)
        {
        }
    }
}

Observe que um método de padrão Dispose não será reconhecido em um parâmetro de tipo que allows ref struct, porque uma interface (e este é o único local onde poderíamos procurar um padrão) não é uma estrutura de referência.

interface IMyDisposable
{
    void Dispose();
}
class C
{
    static void Test<T>(T t, IMyDisposable s) where T : IMyDisposable, allows ref struct
    {
        using (t) // Error, the pattern is not recognized
        {
        }

        using (s) // Error, the pattern is not recognized
        {
        }
    }
}

Declaração de await using

Atualmente, a linguagem não permite o uso de ref structs como recursos na declaração await using. A mesma limitação será aplicada a um parâmetro de tipo que allows ref struct.

Há uma proposta para levantar as restrições gerais em torno do uso de ref structs em métodos assíncronos - https://github.com/dotnet/csharplang/pull/7994. O restante desta seção descreve o comportamento após a limitação geral da declaração await using ser levantada, se/quando isso ocorrer.

Uma instrução await using reconhecerá e usará a implementação da interface IAsyncDisposable quando o recurso for uma estrutura de referência.

ref struct S2 : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync()
    {
    }
}

class C
{
    static async Task Main()
    {
        await using (new S2())
        {
        } // S2.IAsyncDisposable.DisposeAsync
    }
}

Observe que é dada preferência a um método DisposeAsync que implementa o padrão e somente se um não for encontrado, IAsyncDisposable implementação é usada.

Um padrão de método DisposeAsync será reconhecido em um parâmetro de tipo com allows ref struct da mesma forma que é reconhecido em parâmetros de tipo sem essa restrição atualmente.

interface IMyAsyncDisposable
{
    ValueTask DisposeAsync();
}

class C
{
    static async Task Test<T>() where T : IMyAsyncDisposable, new(), allows ref struct
    {
        await using (new T())
        {
        } // IMyAsyncDisposable.DisposeAsync
    }
}

Uma declaração using reconhecerá e utilizará a implementação da interface IAsyncDisposable quando o recurso for um parâmetro de tipo que allows ref struct, o processo de procura pelo método padrão DisposeAsync falhar, e IAsyncDisposable estiver no conjunto de interfaces efetivas do parâmetro de tipo.

interface IMyAsyncDisposable1
{
    ValueTask DisposeAsync();
}

interface IMyAsyncDisposable2
{
    ValueTask DisposeAsync();
}

class C
{
    static async Task Test<T>() where T : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable, new(), allows ref struct
    {
        await using (new T())
        {
            System.Console.Write(123);
        } // IAsyncDisposable.DisposeAsync
    }
}

Declaração de foreach

A secção https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement deve ser atualizada em conformidade, a fim de incorporar o seguinte.

Uma instrução foreach reconhecerá e usará a implementação de IEnumerable<T>/IEnumerable interface quando a coleção for uma ref struct.

ref struct S : IEnumerable<int>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() {...}
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {...}
}

class C
{
    static void Main()
    {
        foreach (var i in new S()) // IEnumerable<int>.GetEnumerator
        {
        }
    }
}

Um padrão de método GetEnumerator será reconhecido em um parâmetro de tipo com allows ref struct da mesma forma que é reconhecido em parâmetros de tipo sem essa restrição atualmente.

interface IMyEnumerable<T>
{
    IEnumerator<T> GetEnumerator();
}

class C
{
    static void Test<T>(T t) where T : IMyEnumerable<int>, allows ref struct
    {
        foreach (var i in t) // IMyEnumerable<int>.GetEnumerator
        {
        }
    }
}

Uma instrução foreach reconhecerá e usará implementação de IEnumerable<T>/IEnumerable interface quando a coleção for um parâmetro de tipo que allows ref struct, o processo de procurar o método de padrão GetEnumerator falhou e IEnumerable<T>/IEnumerable estiver no conjunto de interfaces efetivas do parâmetro de tipo.

interface IMyEnumerable1<T>
{
    IEnumerator<int> GetEnumerator();
}

interface IMyEnumerable2<T>
{
    IEnumerator<int> GetEnumerator();
}

class C
{
    static void Test<T>(T t) where T : IMyEnumerable1<int>, IMyEnumerable2<int>, IEnumerable<int>, allows ref struct
    {
        foreach (var i in t) // IEnumerable<int>.GetEnumerator
        {
        }
    }
}

Um padrão enumerator será reconhecido num parâmetro de tipo que allows ref struct tal como é reconhecido em parâmetros de tipo sem essa restrição atualmente.

interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct 
{
    TEnumerator GetEnumerator();
}

class C
{
    static void Test1<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
        where TEnumerator : IEnumerator, IDisposable, allows ref struct 
    {
        foreach (var i in t) // IEnumerator.MoveNext/Current
        {
        }
    }

    static void Test2<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
        where TEnumerator : IEnumerator<int>, allows ref struct 
    {
        foreach (var i in t) // IEnumerator<int>.MoveNext/Current
        {
        }
    }

    static void Test3<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
        where TEnumerator : IMyEnumerator<int>, allows ref struct 
    {
        foreach (var i in t) // IMyEnumerator<int>.MoveNext/Current
        {
        }
    }
}

interface IMyEnumerator<T> : System.IDisposable
{
    T Current {get;}
    bool MoveNext();
}

Uma instrução foreach reconhecerá e usará a implementação da interface IDisposable quando o enumerador for uma estrutura de referência (*ref struct*).

struct S1
{
    public S2 GetEnumerator()
    {
        return new S2();
    }
}

ref struct S2 : System.IDisposable
{
    public int Current {...}
    public bool MoveNext() {...}
    void System.IDisposable.Dispose() {...}
}

class C
{
    static void Main()
    {
        foreach (var i in new S1())
        {
        } // S2.System.IDisposable.Dispose()
    }
}

Observe que é dada preferência a um método Dispose que implementa o padrão e somente se um não for encontrado, IDisposable implementação é usada.

Uma instrução foreach reconhecerá e usará a implementação da interface IDisposable quando o enumerador for um parâmetro de tipo que allows ref struct e IDisposable está no seu conjunto de interfaces efetivas.

interface ICustomEnumerator
{
    int Current {get;}
    bool MoveNext();
}

interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct 
{
    TEnumerator GetEnumerator();
}

class C
{
    static void Test<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>
        where TEnumerator : ICustomEnumerator, System.IDisposable, allows ref struct 
    {
        foreach (var i in t)
        {
        } // System.IDisposable.Dispose()
    }
}

Observe que um método de padrão Dispose não será reconhecido em um parâmetro de tipo que allows ref struct, porque uma interface (e este é o único local onde poderíamos procurar um padrão) não é uma estrutura de referência. Além disso, como o tempo de execução não fornece uma maneira de verificar, durante a execução, se um parâmetro de tipo que allows ref struct implementa a interface IDisposable, um enumerador de parâmetro de tipo que allows ref struct não será permitido, a menos que IDisposable esteja no seu conjunto de interfaces efetivas.

interface ICustomEnumerator
{
    int Current {get;}
    bool MoveNext();
}

interface IMyDisposable
{
    void Dispose();
}

interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct 
{
    TEnumerator GetEnumerator();
}

class C
{
    static void Test<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>
        where TEnumerator : ICustomEnumerator, IMyDisposable, allows ref struct 
    {
        // error CS9507: foreach statement cannot operate on enumerators of type 'TEnumerator'
        //               because it is a type parameter that allows ref struct and
        //               it is not known at compile time to implement IDisposable.
        foreach (var i in t)
        {
        }
    }
}

Declaração de await foreach

A secção https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement deve ser atualizada em conformidade, a fim de incorporar o seguinte.

Uma instrução await foreach reconhecerá e utilizará a implementação da interface IAsyncEnumerable<T> quando a coleção for uma ref struct.

ref struct S : IAsyncEnumerable<int>
{
    IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token) {...}
}

class C
{
    static async Task Main()
    {
        await foreach (var i in new S()) // S.IAsyncEnumerable<int>.GetAsyncEnumerator
        {
        }
    }
}

Um padrão de método GetAsyncEnumerator será reconhecido em um parâmetro de tipo com allows ref struct da mesma forma que é reconhecido em parâmetros de tipo sem essa restrição atualmente.

interface IMyAsyncEnumerable<T>
{
    IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

class C
{
    static async Task Test<T>() where T : IMyAsyncEnumerable<int>, allows ref struct
    {
        await foreach (var i in default(T)) // IMyAsyncEnumerable<int>.GetAsyncEnumerator
        {
        }
    }
}

Uma instrução await foreach reconhecerá e usará a implementação da interface IAsyncEnumerable<T> quando a coleção for um parâmetro de tipo que corresponda a allows ref struct, o processo de busca pelo método de padrão GetAsyncEnumerator falhar, e IAsyncEnumerable<T> estiver no conjunto de interfaces efetivas do parâmetro de tipo.

interface IMyAsyncEnumerable1<T>
{
    IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

interface IMyAsyncEnumerable2<T>
{
    IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

class C
{
    static async Task Test<T>() where T : IMyAsyncEnumerable1<int>, IMyAsyncEnumerable2<int>, IAsyncEnumerable<int>, allows ref struct
    {
        await foreach (var i in default(T)) // IAsyncEnumerable<int>.GetAsyncEnumerator
        {
            System.Console.Write(i);
        }
    }
}

Uma instrução await foreach continuará a impedir um enumerador de ref struct e um enumerador de parâmetro de tipo que allows ref struct. O motivo é o facto de que o enumerador deve ser preservado nas await MoveNextAsync() chamadas.

Tipo de delegação para a função anônima ou grupo de métodos

A secção https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md#delegate-types indica:

O compilador pode permitir que mais assinaturas se associem aos tipos System.Action<> e System.Func<> no futuro (se os tipos ref struct forem permitidos como argumentos de tipo, por exemplo).

Action<> e Func<> tipos com restrições allows ref struct em seus parâmetros de tipo serão usados em mais cenários envolvendo tipos ref struct na assinatura do delegado.

Se o tempo de execução de destino suportar as restrições allows ref struct, os tipos de delegados anónimos genéricos incluirão a restrição allows ref struct para os seus parâmetros de tipo. Isso permitirá a substituição desses parâmetros de tipo por tipos como ref struct e outros parâmetros de tipo sujeitos à restrição allows ref struct.

Matrizes em linha

A secção https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/inline-arrays.md#detailed-design indica:

A linguagem fornecerá um método seguro de tipos/ref para acessar elementos de tipos de arrays embutidos. O acesso será baseado em span. Isso limita o suporte a tipos de matriz embutidos com tipos de elementos que podem ser usados como um argumento de tipo.

Quando os tipos de span são alterados para suportar vãos de estruturas ref, a limitação deve ser levantada para matrizes embutidas de estruturas ref.

Solidez

Gostaríamos de verificar a solidez tanto do conceito ref struct anti-restrição em particular como do conceito anti-restrição em geral. Para fazer isso, gostaríamos de aproveitar as provas de solidez existentes fornecidas para o sistema do tipo C#. Esta tarefa é facilitada pela definição de uma nova linguagem que é semelhante ao C#, mas mais regular na construção. Verificaremos a segurança desse modelo e, em seguida, especificaremos uma tradução sólida para esse idioma. Como essa nova linguagem é centrada em restrições, chamaremos essa linguagem de "constraint-C#".

O invariante de segurança primário do ref struct que deve ser preservado é que as variáveis do tipo ref struct não devem aparecer no heap. Podemos codificar esta restrição através de uma restrição. Como as restrições permitem a substituição, não a proíbem, definiremos tecnicamente a restrição inversa: heap. A restrição heap especifica que um tipo pode aparecer no heap. Em "constraint-C#" todos os tipos satisfazem a restrição heap, exceto ref-structs. Além disso, todos os parâmetros de tipo existentes em C# serão reduzidos para parâmetros de tipo com a restrição de heap em "constraint-C#".

Agora, supondo que o C# existente seja seguro, podemos transferir as regras ref-struct do C# para "constraint-C#".

  1. Os campos de classes não podem ter um tipo ref-struct.
  2. Os campos estáticos não podem ter um tipo ref-struct.
  3. As variáveis do tipo ref-struct não podem ser convertidas em structs não-ref.
  4. As variáveis do tipo ref-struct não podem ser substituídas como argumentos type.
  5. Variáveis do tipo ref-struct não podem implementar interfaces.

As novas regras aplicam-se à restrição heap:

  1. Os campos de classes devem ter tipos que satisfaçam a restrição de heap.
  2. Os campos estáticos devem ter tipos que satisfaçam a restrição de heap.
  3. Os tipos com a restrição heap têm apenas a conversão de identidade.
  4. As variáveis do tipo ref-struct só podem ser substituídas por parâmetros de tipo sem a restrição de heap.
  5. Os tipos Ref-struct só podem implementar interfaces sem membros de interface padrão.

As regras (4) e (5) são ligeiramente alteradas. Note que a regra (4) não precisa ser transferida exatamente porque temos uma noção de parâmetros de tipo sem a heap contraint. A regra (5) é complicada. A implementação de interfaces não é universalmente incorreta, mas os métodos de interface padrão implicam um recetor do tipo de interface, que é um tipo sem valor e viola a regra (3). Portanto, os membros de interface padrão não são permitidos.

Com essas regras, "constraint-C#" é ref-struct safe, suporta substituição de tipo e suporta implementação de interface. O próximo passo é traduzir a linguagem definida nesta proposta, que podemos chamar de "allow-C#", para "constraint-C#". Felizmente, isso é trivial. O rebaixamento é uma transformação sintática direta. A sintaxe where T : allows ref struct em "allow-C#" é equivalente em "constraint-C#" a não ter restrições, e a ausência de "cláusulas de permissão" é equivalente à restrição heap. Uma vez que a semântica abstrata e a digitação são equivalentes, "allow-C#" também é consistente.

Há uma última propriedade que podemos considerar: se todos os termos digitados em C# também são digitados em "constraint-C#". Em outras palavras, queremos saber se, para todos os termos t em C#, se o termo correspondente t' depois de baixar para "constraint-C#" está bem digitado. Esta não é uma restrição de solidez — tornar termos mal tipificados na nossa linguagem alvo nunca permitiria falta de segurança — pelo contrário, diz respeito à retrocompatibilidade. Se decidirmos usar a digitação de "constraint-C#" para validar "allow-C#", gostaríamos de confirmar que não estamos tornando nenhum código C# existente ilegal.

Como todos os termos do C# começam como termos válidos de "constraint-C#", podemos validar a preservação examinando cada uma de nossas novas restrições "constraint-C#". Em primeiro lugar, o aditamento da restrição heap. Como todos os parâmetros de tipo em C# adquiririam a restrição heap, todos os termos existentes devem satisfazer essa restrição. Isso é verdade para todos os tipos concretos, exceto ref structs, o que é apropriado, uma vez que ref structs podem não aparecer como argumentos de tipo hoje. Também é verdade para todos os parâmetros de tipo, pois todos eles adquiririam a restrição heap. Além disso, uma vez que a restrição heap é uma combinação válida com todas as outras restrições, tal não levantaria quaisquer problemas. As regras (1-5) não apresentariam quaisquer problemas, uma vez que correspondem diretamente às regras C# existentes, ou são flexibilizações das mesmas. Portanto, todos os termos digitáveis em C# devem ser digitáveis em "constraint-C#" e não devemos introduzir nenhuma alteração de quebra de digitação.

Questões em aberto

Sintaxe anti-restrição

Decisão: use where T: allows ref struct

Esta proposta optou por expor a ref struct anti-restrição, aumentando a sintaxe where existente para incluir allows ref struct. Isso descreve sucintamente o recurso e também é expansível para incluir outras anti-restrições no futuro, como ponteiros. Há outras soluções consideradas que merecem ser discutidas.

A primeira é simplesmente escolher outra sintaxe para usar dentro da cláusula where. Outras opções propostas incluíam:

  • ~ref struct: o ~ serve como um marcador de que a sintaxe que se segue é uma anti-restrição.
  • include ref struct: usando includes em vez de allows
void M<T>(T p)
    where T : IDisposable, ~ref struct
{
    p.Dispose();
}

A segunda é usar uma nova cláusula inteiramente para deixar claro que o que se segue é expandir o conjunto de tipos permitidos. Os proponentes desta posição acham que usar a sintaxe dentro de where pode levar a confusão durante a leitura. A proposta inicial utilizava a seguinte sintaxe: allow T: ref struct:

void M<T>(T p)
    where T : IDisposable
    allow T : ref struct
{
    p.Dispose();
}

A sintaxe where T: allows ref struct teve uma preferência um pouco mais forte nas discussões do LDM.

Covariância e contravariância

Decisão: sem novas questões

Para serem maximamente úteis, os parâmetros de tipo que são allows ref struct devem ser compatíveis com a variância genérica. Especificamente, deve ser legal que um parâmetro seja co/contravariante e também allows ref struct. Se faltarem, eles não seriam utilizáveis em muitos dos tipos mais populares delegate e interface em .NET, como Func<T>, Action<T>, IEnumerable<T>, etc.

Após discussão, concluiu-se que se trata de uma não-questão. A restrição de allows ref struct é apenas mais uma forma de como struct pode ser usado como argumento genérico. Assim como um argumento struct normal remove a variância de uma API, o mesmo acontecerá com um ref struct.

Candidatura automática a membros delegados

Decisão: não aplicar automaticamente

Para muitos membros genéricos da delegate, a linguagem pode aplicar automaticamente allows ref struct, como é puramente uma mudança positiva. Considere que, para delegados de estilo Func<> / Action<> e para a maioria das definições de interface, não há desvantagem em permitir a expansão para ref struct. A linguagem pode descrever regras onde é seguro aplicar automaticamente essa anti-restrição. Isso remove o processo manual e aceleraria a adoção desse recurso.

Embora esta aplicação automática de allows ref struct coloque alguns problemas. Primeiro, em cenários de múltiplos alvos. O código seria compilado em uma estrutura de destino, mas falharia em outra e não há nenhum indicador sintático de por que as APIs devem se comportar de forma diferente.

// Works in net9.0 but fails in all other TF
Func<Span<char>> func;

Isto provavelmente levará à confusão entre os clientes, e ao olhar para as mudanças no Func<T> na fonte net9.0, os clientes não teriam nenhuma pista sobre o que mudou.

A outra questão é que mudanças muito sutis no código podem causar ação assustadora à distância problemas. Considere o seguinte código:

interface I1<T>
{
}

Esta interface seria elegível para autoaplicação de allows ref struct. Se um desenvolvedor vier mais tarde e adicionar um método de interface padrão, de repente isso deixaria de ser o caso e romperia qualquer utilização que já tivesse feito invocações como I1<Span<char>>. Esta é uma mudança muito sutil que seria difícil de rastrear.

Alteração de quebra binária

Adicionar allows ref struct a uma API existente não é uma alteração de quebra de fonte. É puramente expandir o conjunto de tipos permitidos para uma API. Necessita verificar se esta alteração provoca uma quebra na compatibilidade binária ou não. Não está claro se a atualização dos atributos de um parâmetro genérico constitui uma alteração de quebra binária.

Aviso sobre a invocação de DIM

O compilador deve avisar sobre a seguinte invocação de M, pois cria a oportunidade para uma exceção de tempo de execução?

interface I1
{
    // Virtual method with default implementation
    void M() { }
}

// Invocation of a virtual instance method with default implementation in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
    where T : allows ref struct, I1
{
    p.M(); // Warn?
}

Isso, no entanto, pode ser barulhento e não muito útil na maioria dos cenários. O C# exigirá ref structs para implementar todas as APIs virtuais. Portanto, supondo que outros jogadores sigam a mesma regra, a única situação em que isso pode causar uma exceção é quando o método é adicionado após o fato. O autor do código que consome muitas vezes não tem conhecimento de todos esses detalhes e frequentemente não tem controle sobre as estruturas ref que serão consumidas pelo código. Portanto, a única ação que o autor pode realmente tomar é suprimir o aviso.

Considerações

Suporte de tempo de execução

Este recurso requer vários apoios da equipa responsável pelo runtime e pelas bibliotecas.

  • Impedindo que métodos de interface padrão sejam aplicados a ref struct
  • API em System.Reflection.Metadata para codificar o valor gpAcceptByRefLike
  • Suporte para parâmetros genéricos sendo um ref struct

É provável que a maior parte deste apoio já esteja em vigor. O ref struct geral como suporte a parâmetros genéricos já está implementado conforme descrito aqui. É possível que a implementação do DIM já leve em conta o ref struct. Mas cada um desses itens precisa ser rastreado.

Controle de versão da API

permite ref struct sem restrições

O allows ref struct anti-restrição pode ser aplicado com segurança a um grande número de definições genéricas que não têm implementações. Isso significa que a maioria dos delegados, interfaces e métodos abstract podem, com segurança, aplicar allows ref struct aos seus parâmetros. Essas são apenas definições de API sem implementações e, portanto, expandir o conjunto de tipos permitidos só resultará em erros se eles forem usados como argumentos de tipo onde ref struct não são permitidos.

Os proprietários de API podem confiar em uma regra simples de "se compilar, é seguro". O compilador irá sinalizar um erro em qualquer uso inseguro de allows ref struct, tal como faz para outros usos de ref struct.

Ao mesmo tempo, os autores da API devem ter em conta as considerações de controle de versão. Essencialmente, os proprietários de API devem evitar adicionar allows ref struct aos parâmetros de tipo, onde o tipo / membro proprietário pode mudar no futuro para ser incompatível com allows ref struct. Por exemplo:

  • Um método abstract que pode mais tarde mudar para um método virtual
  • Um tipo de abstract que, mais tarde, pode adicionar implementações

Nesses casos, um autor de API deve ter cuidado ao adicionar allows ref struct a menos que tenha certeza de que a evolução do tipo/membro não usará T de uma forma que viole as regras de ref struct.

Remover a restrição allows ref struct é sempre uma mudança de rutura: tanto no código-fonte como no binário.

Métodos de interface padrão

Os autores da API precisam estar cientes de que a adição de DIMS interromperá a funcionalidade dos implementadores de ref struct até que sejam recompilados. Isso é semelhante a comportamento DIM existente em que, ao adicionar um DIM a uma interface, as implementações existentes serão interrompidas até que sejam recompiladas. Isso significa que os autores de API precisam considerar a possibilidade de ocorrência de implementações ref struct quando estiverem a adicionar DIMs.

Há três componentes de código que são necessários para criar essa situação:

interface I1
{
    // 1. The addition of a DIM method to an _existing_ interface
    void M() { }
}

// 2. A ref struct implementing the interface but not explicitly defining the DIM 
// method
ref struct S : I1 { }

// 3. The invocation of the DIM method in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
    where T : allows ref struct, I1
{
    p.M();
}

Todos esses três componentes são necessários para criar esse problema específico. Além disso, pelo menos (1) e (2) devem estar em conjuntos diferentes. Se eles estivessem no mesmo assembly, ocorreria um erro de compilação.

UnscopedRef

Adicionar ou remover [UnscopedRef] dos membros de interface é uma alteração que quebra a compatibilidade do código fonte (e pode potencialmente criar problemas em tempo de execução). O atributo deve ser aplicado ao definir um membro da interface e não adicionado ou removido posteriormente.

Span<Span<T>>

Essa combinação de recursos não permite construções como Span<Span<T>>. Isto torna-se um pouco mais claro olhando para a definição de Span<T>:

readonly ref struct Span<T>
{
    public readonly ref T _data;
    public readonly int _length;

    public Span(T[] array) { ... }

    public static implicit operator Span<T>(T[]? array) { }
 
    public static implicit operator Span<T>(ArraySegment<T> segment) { }
}

Se essa definição de tipo incluísse allows ref struct todas as instâncias T na definição precisariam ser tratadas como se fossem potencialmente um tipo ref struct. Isso apresenta duas classes de problemas.

A primeira é para APIs como Span(T[] array) e os operadores implícitos o T não pode ser um ref struct: ele é usado como um elemento de matriz ou como parâmetro genérico que não pode ser allows ref struct. Há um punhado de APIs públicas em Span<T> que têm usos de T que não podem ser compatíveis com um ref struct. Estas são APIs públicas que não podem ser excluídas e, portanto, devem ser racionalizadas pela linguagem. O caminho mais provável é que o compilador trate Span<T> como um caso especial e emita um código de erro já vinculado a uma destas APIs quando o argumento para T for , potencialmente um ref struct.

A segunda razão é que o idioma não suporta ref campos que são ref struct. Há uma proposta de design para permitir esse recurso. Não está claro se isso será aceito na linguagem ou se é expressivo o suficiente para lidar com todo o conjunto de cenários em torno Span<T>.

Ambas as questões estão fora do âmbito da presente proposta.

Lógica de implementação UnscopedRef

A lógica por trás das regras de [UnscopedRef] para implementação de interface é mais fácil de entender ao visualizar o parâmetro this como um argumento explícito, em vez de implícito, para os métodos. Considere, por exemplo, o seguinte struct onde this é visualizado como um parâmetro implícito (semelhante a como o Python lida com ele):

struct S
{
    public void M(scoped ref S this) { }
}

O [UnscopedRef] em um membro da interface está especificando que this não tem scoped para propósitos de duração no site de chamada. Permitir que [UnscopedRef] seja omitido no membro implementador é efetivamente permitir que um parâmetro que é ref T seja implementado por um parâmetro que é scoped ref T. A linguagem já permite isso:

interface I1
{
    void M(ref Span<char> span);
}

struct S : I1
{
    public void M(scoped ref Span<char> span) { }
}

Artigos relacionados: