Skip to main content

Noções básicas sobre métodos de interface padrão | .NET8 | CSharp

  • Noções básicas sobre métodos de interface padrão | .NET8 | CSharp

Noções básicas sobre métodos de interface padrão

Os métodos de interface padrão foram introduzidos no C# 8 , principalmente como uma forma de facilitar a evolução interfacesem interromper as pessoas que implementaram a interface. O recurso permite fornecer um corpo de método ao definir um método em uma interface, semelhante a como você pode usar uma abstractclasse.

Os membros de interface padrão também permitem uma interoperabilidade mais fácil com recursos semelhantes que existem no Android e no iOS . Eles também oferecem algumas das características de traits , que são uma abordagem para construir sistemas que se concentram na componibilidade em vez da herança .

É mais fácil entender o recurso com um exemplo, então vou usar uma versão simplificada do cenário na documentação da Microsoft . Digamos que você definiu a seguinte interface em uma biblioteca:

public interface ICustomer
{
DateTime DateJoined { get; }
string Name { get; }
}

Você envia essa interface como parte de sua funcionalidade e os clientes a usam. Eles têm diversas classes que implementam essa interface, uma das quais é mostrada abaixo:

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
public Guid Id { get; } = id;
public string Name { get; } = name;
public DateTime DateJoined { get; } = dateJoined;
}

O código acima usa construtores primários para reduzir alguns dos padrões do construtor;

Tudo está bem, até você perceber que precisa evoluir sua interface.

Evoluir a interface é uma mudança radical

Mais tarde, você decide que precisa adicionar outro método à sua interface, GetLoyaltyDiscount()que retorne o desconto que um determinado cliente recebe. Em última análise, você deseja uma interface parecida com esta:

public interface ICustomer
{
DateTime DateJoined { get; }
string Name { get; }
decimal GetLoyaltyDiscount();// 👈 Adding this
}

Infelizmente, fazer essa mudança é uma mudança radical. Se você fizesse essa alteração, quando os consumidores da sua biblioteca fossem atualizados, o código deles não seria mais compilado até que implementassem o novo método.

Se você estiver seguindo o versionamento semântico (semver) - e provavelmente deveria estar se for um autor de biblioteca - então você pode fazer essa mudança em um grande salto de versão. Mas ainda é indesejável. Isso adiciona atrito aos consumidores de sua biblioteca, acrescentando uma etapa que eles precisam realizar apenas para atualizar sua biblioteca. Se você puder evitar alterações significativas, isso geralmente é uma coisa boa.

Aaron Stannard tem uma ótima série sobre " Professional Open Source " e como ela se relaciona com o versionamento. Eu particularmente gosto de sua postagem sobre semver "prático" versus "estrito", pois ele lida com as complexidades das mudanças significativas em bibliotecas populares de código aberto e como pensar sobre elas.

Dependendo de seus requisitos exatos, você poderá escrever GetLoyaltyDiscount()simplesmente como um método de extensão que opera em ICustomer. Mas se você precisa que os consumidores possam substituir essa funcionalidade, você ficará meio preso. É aí que entram os métodos de interface padrão.

Métodos de interface padrão para o resgate

Os métodos de interface padrão permitem que você faça essa alteração sem interromper os consumidores da sua biblioteca, fornecendo uma implementação concreta para o método. Por exemplo:

public interface ICustomer
{
DateTime DateJoined { get; }
string Name { get; }
// 👇 Provide a body for the new methoddecimal GetLoyaltyDiscount()
{
var twoYearsAgo = DateTime.UtcNow.AddYears(-2);
if (DateJoined < twoYearsAgo)
{
// Customers who joined > 2 years ago get a 10% discountreturn 0.10m;
}// Otherwise no discountreturn 0;
}
}

Vamos parar por um momento para pensar em como isso é estranho .

Estamos definindo um interfaceaqui, não um classou struct. No entanto, temos um corpo de método.

Antes do C# 8, você não podia fazer isso e, além do mais, também não fazia sentido . interfaces eram literalmente isso: definições da API/interface que um tipo deve implementar. Se você quisesse fornecer uma implementação "padrão", teria que usar uma abstract class, o que o limitava de outras maneiras.

Comparo métodos de interface padrão com classes abstratas mais brevemente.

Então, digamos que você adicione esse método padrão à sua interface e o envie em uma nova versão da biblioteca. Você não precisa fazer um aumento na versão principal porque esta não é uma alteração significativa. A implementação existente do consumidor (mostrada novamente abaixo) ainda é válida, mesmo que não defina GetLoyaltyDiscount():

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
public Guid Id { get; } = id;
public string Name { get; } = name;
public DateTime DateJoined { get; } = dateJoined;
// GetLoyaltyDiscount() is not implemented
}

O código do consumidor ainda é compilado e, quando a biblioteca precisa usar o ICustom.GetLoyaltyDiscount()método, ela usa a implementação padrão.

Posteriormente, se o consumidor decidir que deseja implementar GetLoyaltyDiscount(), ele poderá fazê-lo:

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
public Guid Id { get; } = id;
public string Name { get; } = name;
public DateTime DateJoined { get; } = dateJoined;
public decimal GetLoyaltyDiscount() => 0;// Never give a discount
}

Então, se você puder implementar uma mudança em uma interface como um método de interface padrão, você poderá obter o melhor dos dois mundos: uma interface em evolução que não causa mudanças drásticas.

O recurso de métodos de interface padrão habilitou mais do que apenas o recurso acima. Agora você pode até definir privatemembros em um campo que pode ser usado como parte da implementação de métodos de interface padrão, bem como definir staticmétodos. A documentação mostra um exemplo desses recursos aqui

Se tudo isso parece bom demais para ser verdade, você não ficará surpreso ao saber que existem algumas arestas a serem observadas ao trabalhar com métodos padrão. Na próxima seção, discuto algumas coisas para pensar e observar

Bordas nítidas a serem observadas com métodos de interface padrão

Esta seção discute alguns itens a serem considerados ao trabalhar com métodos de interface padrão.

Os métodos de interface padrão não são herdados

Embora interfaces que usam métodos de interface padrão pareçam muito com classes abstratas, eles são fundamentalmente diferentes. Uma grande diferença é que métodos de interface padrão não são herdados por implementadores da interface.

Por exemplo, imagine que você está trabalhando com o método SampleCustomerpersonalizado antes de adicionar GetLoyaltyDiscount():

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
public Guid Id { get; } = id;
public string Name { get; } = name;
public DateTime DateJoined { get; } = dateJoined;
// GetLoyaltyDiscount() is not implemented
}

O código a seguir que usa a implementação de interface padrão de GetLoyaltyDiscount()compilação e impressão 0.10:

// Create a customer that joined 3 years ago
SampleCustomer customer = new SampleCustomer(Guid.Empty, "me", DateTime.Now.AddYears(-3));
PrintDiscount(customer); // Prints "0.10"

static void PrintDiscount(ICustomer customer)
=> Console.WriteLine(customer.GetLoyaltyDiscount()); // Use the default interface method

No entanto, o seguinte não é compilado:

// Create a customer that joined 3 years ago
SampleCustomer customer = new SampleCustomer(Guid.Empty, "me", DateTime.Now.AddYears(-3));

Console.WriteLine(customer.GetLoyaltyDiscount()); // 💥 The GetLoyaltyDiscount() method does not exist!

O problema é que o GetLoyaltyDiscount()método não é herdado por SampleCustomer. O método não existe SampleCustomer, então você não pode chamá-lo.

No entanto, o seguinte, no qual lançamos o SampleCustomerto an, ICustomer é compilado e impresso 0.10conforme esperado:

ICustomer customer = new SampleCustomer(Guid.Empty, "me", DateTime.Now.AddYears(-3));
// 👆 cast to ICustomer

Console.WriteLine(customer.GetLoyaltyDiscount()); // Prints 0.10

Portanto, a lição aqui é que você pode precisar lançar to ICustomerpara chamar métodos de interface padrão. Mas as coisas ficam ainda mais confusas se você começar a adicionar mais tipos à hierarquia...

Entender qual método será chamado é…complicado

OK, um quebra-cabeça rápido. O que este programa imprime?

using System;

// 👇 casting to IShape
IShape shape = new Square();
Console.WriteLine(shape.GetName()); // 👈 What does this print?

public interface IShape
{
string GetName() => "IShape"; // Default implementation
}

public class Rectangle : IShape
{
}

public class Square : Rectangle
{
public string GetName() => "Square"; // Specific implementation
}

A resposta é que isso imprime IShape, embora Squaretenha fornecido uma implementação predominante para o GetName()método! O Rectangletipo na hierarquia implementa IShapee não substitui o comportamento de GetName(), portanto, o compilador usa a implementação padrão.

A melhor maneira de "consertar" esse comportamento é garantir que você implemente explicitamente a IShapeinterface, Square mesmo que ela seja implementada implicitamente, herdando de Rectangle:

// Expicitly implement the interface 👇
public class Square : Rectangle, IShape
{
public string GetName() => "Square"; // This now overrides the default implementation
}

Agora, se você executar o programa acima, o código será impresso Square, como você esperava originalmente! IMO, esta é uma das vantagens mais nítidas dos métodos de interface padrão, já que você realmente não recebe nenhuma ajuda do compilador sobre esse comportamento não intuitivo.

O problema da herança do diamante

Felizmente, para alguns problemas de método de interface padrão, o compilador fornece alguma ajuda. Um desses problemas é o problema do diamante , que é um problema bem conhecido associado à herança múltipla.

O exemplo a seguir mostra uma demonstração simples do problema:

interface IShape
{
string GetName() => "IShape"; // Default implementation
}

interface IHasStraightEdges : IShape
{
string IShape.GetName() => "IHasStraightEdges"; // Override the default
}

interface IHasCurvedEdges : IShape
{
string IShape.GetName() => "IHasCurvedEdges"; // Override the default
}

// inherits both interfaces
public class MySemiCircle : IHasStraightEdges, IHasCurvedEdges {}

O código acima não será compilado; em vez disso, você receberá um erro:

Interface member `IShape.GetName()` does not have a most specific
implementation. Neither `IHasStraightEdges.IShape.GetName()` or
`IHasCurvedEdges.IShape.GetName()` is most specific.

É fácil visualizar o problema pensando no que o seguinte imprimiria:

IShape shape = new MySemiCircle();// cast to IShape
Console.WriteLine(shape.GetName());// What should this print?!

O compilador não possui um "melhor" método para chamar, então desiste. Este é o clássico diamante da morte. O compilador força você a substituir especificamente o método, por exemplo:

// Optionally Implement IShape explicitly  
👇public class MySemiCircle : IHasStraightEdges, IHasCurvedEdges, IShape{
public string GetName() => "MySemiCircle";// 👈 Provide an implementation (required
)}

Estruturas que implementam interfaces são problemáticas

A implementação de an interfacenão está limitada a a class, você structtambém pode implementá-la em a. Infelizmente, structnão funcionam bem com os métodos de interface padrão :

  • Para usar um método de interface padrão, você deve converter para a interface, por exemplo (IShape)myStructboxingstructstruct

    . No entanto, converter para uma interface requer

    o

    , alocando no heap e provavelmente invalidando um dos principais motivos pelos quais

    s são usados em C# (para reduzir alocações)

  • Se o método de interface padrão modificarnão

    o estado do objeto, ele estará operando na cópia em caixa da estrutura, portanto,

    ocorrerão modificações no original. É improvável que este seja o comportamento desejado.

Infelizmente, não há muito que você possa fazer sobre essas limitações. Uma boa regra é simplesmente não usar a structpara implementar interfaces que usam métodos de interface padrão.

Claro, o objetivo dos métodos de interface padrão é que você comece sem eles e depois os adicione mais tarde, o que significa que esse problema pode surgir do nada.

Os métodos de interface padrão requerem suporte de tempo de execução

A implementação de métodos de interface padrão não exigiu apenas alterações no compilador C#, mas também no tempo de execução do .NET . Essas alterações foram adicionadas ao tempo de execução CoreCLR (e mono) no .NET Core 3.0 quando o C# 8 foi lançado, portanto, você não pode usá-las no .NET Core 1.x ou .NET Core 2.x (sim, as pessoas ainda estão usando estes 😅).

Matt Warren tem uma ótima postagem mostrando como o recurso foi implementado em tempo de execução se você quiser se aprofundar no assunto!

Mais importante ainda, você também não pode usar membros de interface padrão no .NET Framework. Isso significa que se o seu projeto de código aberto ainda for direcionado ao .NET Framework, você não poderá usar membros de interface padrão.

Resumo

Neste post descrevi o recurso de métodos de interface padrão do C# 8. Esse recurso permite habilitar interfaceimplementações sem interromper os consumidores da interface, permitindo que você forneça um corpo aos membros da interface. Isso fornece um mecanismo totalmente novo para compartilhar funcionalidade entre vários tipos, mas também pode fornecer alguns casos extremos complicados, porque a implementação da interface padrão não é herdada pelos implementadores da interface. Na próxima postagem, discuto um caso de uso em que métodos de interface padrão foram usados para melhorar o desempenho do ASP.NET Core