Interfaces de estrutura de referência

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela 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 divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas LDM (reunião de design de idioma) pertinentes.

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.

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

Resumo

Esta proposta expandirá os recursos de tal forma que eles possam implementar interfaces e participar como argumentos de ref struct tipo genérico.

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 levam 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 ref struct a implementação de interfaces permitirá que as operações sejam abstraídas sobre elas como são para outros tipos.

Design detalhado

interfaces de struct ref

A linguagem permitirá que ref struct os tipos implementem interfaces. A sintaxe e as regras são as mesmas do normal struct , com algumas exceções para levar em conta as limitações dos ref struct tipos.

A capacidade de implementar interfaces não afeta as limitações existentes em relação às instâncias de boxe ref struct . Isso significa que, mesmo que uma ref struct interface em particular seja implementada, ela não poderá ser diretamente convertida nela, 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 participar de argumentos genéricos (conforme exposto posteriormente).

Para permitir que as interfaces cubram toda a expressividade de um ref struct e os problemas de tempo de vida que eles podem apresentar, a linguagem permitirá que [UnscopedRef] apareçam nos métodos e propriedades da interface. Isso é necessário, pois permite que interfaces que abstraem 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 struct / ref struct membro implementa um membro de interface com um [UnscopedRef] atributo, o membro de implementação 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 tenha 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, pois ref struct não há proteções contra a implementação padrão encaixotando o this membro.

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 tenham implementações padrão. O runtime também será atualizado para gerar uma exceção se um membro de interface padrão for chamado em um ref struct tipo.

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 ref struct. Este é 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 de instância virtual (não abstrato) (ou propriedade) em um parâmetro de tipo que permite ref struct.

Anotações detalhadas:

  • A ref struct pode implementar uma interface
  • A ref struct não pode participar de membros de interface padrão
  • A 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 dentro de allows ref struct uma where cláusula:

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 where cláusula, pois especifica os recursos do parâmetro genérico. A diferença é que outros itens de sintaxe limitam o conjunto de tipos que podem atender a um parâmetro genérico enquanto allows ref struct expandem 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 associado por allows ref struct tem todos os comportamentos de um ref struct tipo:

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

Exemplos dessas 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 de tipo. Por exemplo, S no código abaixo não pode ser substituído por um ref struct:

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

Notas detalhadas:

  • Um where T : allows ref struct parâmetro genérico não pode
    • Ter where T : U onde U é um tipo de referência conhecido
    • Ter where T : class restrição
    • 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
  • Deve allows ref struct ser a última restrição na where cláusula
  • Um parâmetro T de tipo que tem allows ref struct todas as mesmas limitações que um ref struct tipo.

Representação em metadados

Os parâmetros de tipo que permitem structs ref serão codificados em metadados, conforme descrito no documento genérico semelhante a byref. Especificamente usando o valor do CorGenericParamAttr.gpAllowByRefLike(0x0020) sinalizador orSystem.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020). Se o tempo de execução dá suporte ao recurso pode ser determinado verificando a presença do System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics campo. As APIs foram adicionadas no https://github.com/dotnet/runtime/pull/98070.

Instrução using

Uma using instrução reconhecerá e usará a implementação da IDisposable interface quando o recurso for um 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 a preferência é dada a um Dispose método que implementa o padrão e, somente se um não for encontrado, IDisposable a implementação é usada.

Uma using instrução reconhecerá e usará a implementação de interface quando resource for um parâmetro de IDisposable tipo que allows ref struct e IDisposable estiver em 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 porque allows ref struct uma interface (e este é o único lugar onde poderíamos procurar um padrão) não é uma estrutura ref.

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
        {
        }
    }
}

Instrução await using

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

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

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

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

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

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

Um método de padrão DisposeAsync será reconhecido em um parâmetro de tipo que allows ref struct , como é reconhecido em parâmetros de tipo sem essa restrição hoje.

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 using instrução reconhecerá e usará a implementação de interface quando resource for um parâmetro de IAsyncDisposable tipo que allows ref struct, o processo de procura DisposeAsync do método de padrão falhou e IAsyncDisposable está 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
    }
}

Instrução foreach

A https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement seção deve ser atualizada de acordo para incorporar o seguinte.

Uma foreach instrução reconhecerá e usará a implementação da IEnumerable<T>/IEnumerable interface quando a coleta for uma estrutura ref.

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 método de padrão GetEnumerator será reconhecido em um parâmetro de tipo que allows ref struct , como é reconhecido em parâmetros de tipo sem essa restrição hoje.

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 foreach instrução reconhecerá e usará a implementação de interface quando a coleta for um parâmetro deIEnumerable<T>/IEnumerable tipo queallows ref struct, o processo de procura GetEnumerator do método de padrão falhou e IEnumerable<T>/IEnumerable está 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 enumerator padrão será reconhecido em um parâmetro de tipo que allows ref struct , como é reconhecido em parâmetros de tipo sem essa restrição hoje.

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 foreach instrução reconhecerá e usará a implementação da IDisposable interface quando o enumerador for um 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 a preferência é dada a um Dispose método que implementa o padrão e, somente se um não for encontrado, IDisposable a implementação é usada.

Uma foreach instrução reconhecerá e usará a implementação de interface quando enumerator for um parâmetro de IDisposable tipo que allows ref struct está IDisposable em 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 porque allows ref struct uma interface (e este é o único lugar onde poderíamos procurar um padrão) não é uma estrutura ref. Além disso, como o runtime não fornece uma maneira de verificar se em runtime um parâmetro de tipo que allows ref struct implementa interface, um enumerador de IDisposable parâmetro de tipo que não será permitido, a menos allows ref struct que IDisposable esteja em 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)
        {
        }
    }
}

Instrução await foreach

A https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement seção deve ser atualizada de acordo para incorporar o seguinte.

Uma await foreach instrução reconhecerá e usará a implementação da IAsyncEnumerable<T> interface quando a coleta for uma estrutura ref.

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 método de padrão GetAsyncEnumerator será reconhecido em um parâmetro de tipo que allows ref struct , como é reconhecido em parâmetros de tipo sem essa restrição hoje.

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 await foreach instrução reconhecerá e usará a implementação de interface quando a coleta for um parâmetro de IAsyncEnumerable<T> tipo que allows ref struct, o processo de procura GetAsyncEnumerator do método de padrão falhou e IAsyncEnumerable<T> está 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 await foreach instrução continuará não permitindo um enumerador ref struct e um enumerador de parâmetro de tipo que allows ref struct. O motivo é o fato de que o enumerador deve ser preservado entre await MoveNextAsync() chamadas.

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

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

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

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

Se o runtime de destino der suporte a allows ref struct restrições, os tipos de delegado anônimo genéricos incluirão allows ref struct restrição para seus parâmetros de tipo. Isso permitirá a substituição desses parâmetros de tipo por tipos ref struct e outros parâmetros de tipo com allows ref struct restrição.

Matrizes embutidas

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

A linguagem fornecerá uma maneira segura para tipo e referência para acessar elementos de tipos de matriz embutida. O acesso será baseado em intervalos. Isso limita o suporte a tipos de matriz embutida com tipos de elemento que podem ser usados como um argumento de tipo.

Quando os tipos de span são alterados para dar suporte a spans de ref structs, a limitação deve ser removida para matrizes embutidas de ref structs.

Solidez

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

A invariável de segurança ref struct primária que deve ser preservada é que as variáveis do tipo ref struct não devem aparecer no heap. Podemos codificar essa restrição por meio 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 heap restrição 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 heap restrição 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. Variáveis do tipo ref-struct não podem ser convertidas em structs não ref.
  4. Variáveis do tipo ref-struct não podem ser substituídas como argumentos de tipo.
  5. Variáveis do tipo ref-struct não podem implementar interfaces.

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

  1. Os campos de classes devem ter tipos que satisfaçam a heap restrição.
  2. Os campos estáticos devem ter tipos que satisfaçam a heap restrição.
  3. Os tipos com a heap restrição 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 heap restrição.
  5. Os tipos ref-struct só podem implementar interfaces sem default-interface-members.

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

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

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 t' correspondente após a redução para "constraint-C#" é bem tipado. Isso não é uma restrição de solidez - tornar os termos mal digitados em nosso idioma de destino nunca permitiria insegurança - em vez disso, diz respeito à compatibilidade com versões anteriores. Se decidirmos usar a digitação de "constraint-C#" para validar "allow-C#", gostaríamos de confirmar que não estamos tornando ilegal nenhum código C# existente.

Como todos os termos C# começam como termos "constraint-C#" válidos, podemos validar a preservação examinando cada uma de nossas novas restrições "constraint-C#". Primeiro, a adição da heap restrição. Como todos os parâmetros de tipo em C# adquiririam a heap restrição, todos os termos existentes devem satisfazer essa restrição. Isso é verdadeiro para todos os tipos concretos, exceto ref structs, o que é apropriado, pois ref structs podem não aparecer como argumentos de tipo hoje. Também é verdadeiro para todos os parâmetros de tipo, uma vez que todos eles próprios adquiririam a heap restrição. Além disso, como a heap restrição é uma combinação válida com todas as outras restrições, isso não apresentaria nenhum problema. As regras (1-5) não apresentariam nenhum problema, pois correspondem diretamente às regras existentes do C# ou são relaxamentos 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 interruptiva de digitação.

Problemas Abertos

Sintaxe anti-restrição

Decisão: usar where T: allows ref struct

Esta proposta optou por expor a ref struct anti-restrição aumentando a sintaxe existente where para incluir allows ref struct. Isso descreve sucintamente o recurso e também pode ser expandido para incluir outras antirestrições no futuro, como ponteiros. Existem outras soluções consideradas que valem a pena discutir.

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

  • ~ref struct: o ~ serve como um marcador de que a sintaxe a seguir é 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 disso acham que o uso da sintaxe pode where levar à confusão durante a leitura. A proposta inicial usava a seguinte sintaxe: allow T: ref struct

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

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

Co e contra variância

Decisão: sem novos problemas

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. Na falta disso, 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 isso não é um problema. A allows ref struct restrição é apenas outra maneira que struct pode ser usada como argumentos genéricos. Assim como um argumento normal struct remove a variação de uma API, o mesmo acontecerá com um ref struct.

Aplicação automática para delegar membros

Decisão: não aplicar automaticamente

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

Esta aplicação automática de allows ref struct apresenta alguns problemas. O primeiro é em cenários multi-alvo. 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 maneira diferente.

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

É provável que isso leve à confusão do cliente e observar as Func<T> alterações na net9.0 fonte não daria aos clientes nenhuma pista sobre o que mudou.

O outro problema é que mudanças muito sutis no código podem causar problemas de ação assustadora à distância . Considere o seguinte código:

interface I1<T>
{
}

Essa interface seria elegível para aplicação automática de allows ref struct. Se um desenvolvedor aparecer mais tarde e adicionar um método de interface padrão, de repente não será e quebrará todos os consumidores que já criaram invocações como I1<Span<char>>. Esta é uma mudança muito sutil que seria difícil de rastrear.

Alteração da falha binária

Adicionar allows ref struct a uma API existente não é uma alteração significativa na origem. Ele está puramente expandindo o conjunto de tipos permitidos para uma API. Precisa rastrear se esta é uma alteração de quebra 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 interruptiva binária.

Avisar sobre a invocação do DIM

O compilador deve avisar sobre a seguinte invocação de como ele cria a oportunidade para uma exceção de M runtime?

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á structs ref 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 de consumo geralmente não tem conhecimento de todos esses detalhes e geralmente 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 a tempo de execução

Esse recurso requer vários suportes da equipe de runtime/bibliotecas:

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

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

Controle de versão da API

permite ref struct anti-restrição

A allows ref struct anti-restrição pode ser aplicada 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 abstract métodos podem ser aplicados allows ref struct com segurança 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 forem usados como argumentos de tipo onde ref struct não são permitidos.

Os proprietários de API podem contar com uma regra simples de "se compilar, é seguro". O compilador apresentará erros em qualquer uso inseguro do allows ref struct, assim como em outros ref struct usos.

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

  • Um abstract método que pode posteriormente mudar para um virtual método
  • Um abstract tipo que pode adicionar implementações posteriormente

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 será usada T de uma forma que quebre ref struct as regras.

Remover a allows ref struct anti-restrição é sempre uma alteração significativa: fonte e binário.

Métodos de interface padrão

Os autores da API precisam estar cientes de que a adição de DIMS interromperá ref struct os implementadores até que eles sejam recompilados. Isso é semelhante ao comportamento do 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 da API precisam considerar a probabilidade de ref struct implementações ao adicionar DIMs.

Há três componentes de código 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] de membros é uma alteração significativa na origem (e potencialmente criando problemas de interface tempo de execução). O atributo deve ser aplicado ao definir um membro da interface e não adicionado ou removido posteriormente.

Intervalo de extensão<<T>>

Essa combinação de recursos não permite construções como Span<Span<T>>. Isso fica 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 fosse incluída allows ref struct , todas as T instâncias na definição precisariam ser tratadas como se fossem potencialmente um ref struct tipo. Isso apresenta duas classes de problemas.

A primeira é para APIs como Span(T[] array) e os operadores implícitos não T podem ser um ref struct: é 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 que Span<T> têm usos que T não podem ser compatíveis com um ref struct. Essas são APIs públicas que não podem ser excluídas e, portanto, devem ser racionalizadas pela linguagem. O caminho mais provável a seguir é que o compilador emitirá um caso Span<T> especial e emitirá um código de erro vinculado a uma dessas APIs quando o argumento for Tpotencialmente um ref struct.

A segunda é que a linguagem não suporta ref campos que são ref struct. Existe 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 o conjunto completo de cenários em torno Span<T>de .

Ambas as questões estão além do escopo desta proposta.

Lógica de implementação UnscopedRef

A lógica por trás das regras para implementação de [UnscopedRef] interface é mais fácil de entender ao visualizar o this parâmetro 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 isso):

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

O [UnscopedRef] membro de interface on está especificando que this falta para fins de scoped vida útil no site de chamada. Permitir [UnscopedRef] que seja omitido no membro de implementação é efetivamente permitir que um parâmetro seja ref T implementado por um parâmetro que seja 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) { }
}

Itens relacionados: