Implicit Operators

O implicit operator permite adicionar comportamentos de conversão a tipos complexos no C#, de forma que o compilador consiga convertê-los automaticamente, sem nenhum cast. Com ele, você escreve menos código, deixa as atribuições mais naturais e, em muitos casos, dispensa bibliotecas externas de mapeamento.

Neste artigo, você vai entender a diferença entre conversões implícitas e explícitas, criar seus próprios operadores e aplicá-los em Value Objects, strings, DTOs e ViewModels.

O que são conversões explícitas e implícitas

Conversões implícitas são aquelas que o compilador faz de forma automática, porque ele já sabe resolvê-las naturalmente. Por exemplo, veja a conversão de int para double:

int x = 100;
double y = x;

Nesse caso, o valor de x passa de int para double sem nenhum esforço. Afinal, os números reais comportam qualquer número inteiro, então não existe risco de perder informação.

Porém, o mesmo não acontece no caminho inverso. O código abaixo nem compila:

double x = 100.0;
int y = x; // erro de compilação

Os números inteiros não conseguem representar sozinhos as casas decimais de um número real. Por isso, o compilador exige que você deixe a conversão explícita, assumindo o risco de perda:

double x = 100.98;
int y = (int)x;

Console.WriteLine(y);
// Imprime 100 na tela, ignorando o que vem depois do "."

Esse (int) é o cast, ou seja, a conversão explícita. Ele avisa ao compilador que você sabe que a parte decimal será descartada.

Em resumo, a regra do C# é clara. Conversões seguras acontecem de forma implícita, enquanto conversões que podem falhar ou perder dados precisam ser explícitas.

O que é o implicit operator no C#

O implicit operator é a forma de dizer ao compilador como resolver uma conversão de maneira implícita para os seus próprios tipos. Com isso, você reduz a quantidade de código escrita em cada atribuição.

Ele faz parte dos operadores de conversão definidos pelo usuário. Além do implicit, existe o explicit, que exige o cast, assim como acontece de double para int.

A sintaxe segue sempre o mesmo formato. O método é public static, usa a palavra-chave implicit operator e tem como nome o tipo de destino:

public static implicit operator TargetType(SourceType value)
{
    // Lógica de conversão
}

Pense no cenário do double para int. A ideia do implicit operator é justamente criar um método dizendo como essa conversão deve acontecer, por exemplo ignorando os valores depois do ponto.

No entanto, existe uma restrição importante. Segundo a documentação oficial, o tipo que declara a conversão precisa ser o tipo de origem ou o tipo de destino dela.

Por isso, você não consegue criar um operador entre double e int diretamente, já que ambos pertencem ao .NET. Na prática, você aplica o implicit operator nas suas classes, structs e records.

Regras do implicit operator

Antes de sair criando operadores, vale conhecer as regras que a linguagem impõe. Elas evitam ambiguidades e comportamentos inesperados.

Primeiro, o operador só pode existir quando não há uma conversão padrão entre os dois tipos. Ou seja, você não redefine conversões que o próprio C# já oferece.

Além disso, a documentação recomenda que conversões implícitas nunca lancem exceções e nunca percam informação. Se a conversão puder falhar, defina-a como explicit, porque assim quem chama precisa escrever o cast de forma consciente.

Outro detalhe pouco conhecido envolve os operadores is e as. Eles não consideram conversões definidas pelo usuário, então apenas a atribuição ou o cast disparam o seu operador.

Se quiser revisar como esses operadores funcionam, o artigo sobre is e as no C# explica cada um deles em detalhes.

Implicit operator com strings

O exemplo anterior serviu apenas para entender o conceito. Convenhamos, escrever um método só para evitar um (int)x seria exagero e não traria ganho real.

Um uso muito mais interessante aparece na conversão entre objetos e strings. Algo comum nas classes é sobrescrever o método ToString, que todo objeto do .NET possui.

Isso é possível porque todo tipo do .NET deriva de System.Object. Essa classe base declara o ToString com o modificador virtual, o que permite reescrevê-lo nas classes filhas.

Sobrescrevendo o ToString em um Value Object

O cenário a seguir é bem comum nos cursos do balta.io. Temos um Value Object de telefone com o método ToString sobrescrito:

public class Phone
{
    public string CountryCode { get; set; } = string.Empty;
    public string Area { get; set; } = string.Empty;
    public string Number { get; set; } = string.Empty;

    public override string ToString()
    {
        return $"+{CountryCode} ({Area}) {Number}";
    }
}

Dessa forma, sempre que quiser exibir o telefone, basta chamar phone.ToString(). Além disso, a interpolação $"My phone is {phone}" também usa o ToString automaticamente.

Para entender melhor esse tipo de objeto, o artigo sobre como criar bons Value Objects em C# aprofunda o assunto.

Convertendo o ToString para implicit operator

Esse cenário pede uma refatoração em que o implicit operator cai muito bem. Ele pode converter tanto o telefone em string quanto uma string em telefone:

public record Phone(string CountryCode, string Area, string Number)
{
    // Converte um Phone em string
    public static implicit operator string(Phone phone)
        => $"+{phone.CountryCode} ({phone.Area}) {phone.Number}";

    // Converte uma string no formato "55 11 999999999" em Phone
    public static implicit operator Phone(string phone)
    {
        var data = phone.Split(" ");
        return new Phone(data[0], data[1], data[2]);
    }
}

Repare que a classe virou um record com construtor primário. Assim, o código fica mais curto e o objeto ganha igualdade por valor, algo natural para um Value Object.

O primeiro operador transforma o objeto em string. Já o segundo faz o caminho inverso, quebrando a string pelos espaços e montando um novo Phone.

Usando a conversão na prática

Agora o objeto aceita receber uma string e também se converte automaticamente em uma:

// Cria um telefone (objeto)
var phone = new Phone("55", "11", "999999999");
// Converte para string
Console.WriteLine(phone);

// Cria uma string
var telephone = "55 11 987765544";
// Converte para um telefone (objeto)
phone = telephone;
Console.WriteLine(phone);

// RESULTADO
// +55 (11) 999999999
// +55 (11) 987765544

Na atribuição phone = telephone, o compilador encontra o implicit operator de string para Phone e o executa sem nenhum cast. Da mesma forma, sempre que um Phone aparece onde se espera uma string, o outro operador entra em ação.

Contudo, lembre-se da recomendação da documentação. Uma string fora do formato esperado faria o Split gerar menos de três partes, e o acesso a data[2] lançaria uma exceção.

Por isso, em código de produção, valide a entrada antes de converter ou troque a conversão de string para Phone por um operador explicit. Outra opção é oferecer um método TryParse, como fazem os tipos numéricos do .NET.

Implicit operator em DTOs e ViewModels

Chega de AutoMapper! Brincadeira, mas quem acompanha os cursos e conteúdos do balta.io já percebeu que o AutoMapper quase nunca aparece nos projetos.

O motivo é simples: dá para converter os objetos diretamente, sem Reflection. Talvez não seja tão produtivo quanto o AutoMapper, mas a performance compensa essa perda de produtividade na maioria dos casos.

Além disso, o mapeamento manual fica explícito no código. Então, qualquer pessoa do time encontra a regra de conversão com um simples "ir para a definição" na IDE.

A entidade e o ViewModel

Suponha uma entidade chamada User com as seguintes propriedades:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public bool Active { get; set; }
}

Agora queremos exibir apenas algumas propriedades desse usuário, ocultando a senha, por exemplo. Para isso, criamos um DTO ou ViewModel:

public class UserViewModel
{
    public UserViewModel(string userName)
        => UserName = userName;

    public string UserName { get; }
}

O ViewModel expõe apenas o nome de usuário. Dessa forma, a senha nunca sai da camada de domínio por engano.

Criando o implicit operator no ViewModel

Agora usamos o implicit operator para converter um User em UserViewModel, assim como fizemos com o telefone:

public static implicit operator UserViewModel(User user)
    => new UserViewModel(user.Username);

Como resultado, o UserViewModel completo fica assim:

public class UserViewModel
{
    public UserViewModel(string userName)
        => UserName = userName;

    public string UserName { get; }

    // Converte a entidade User em UserViewModel
    public static implicit operator UserViewModel(User user)
        => new UserViewModel(user.Username);
}

Note que o operador fica dentro do UserViewModel, que é o tipo de destino. Isso respeita a regra de que um dos dois tipos da conversão precisa declarar o operador.

Convertendo a entidade sem AutoMapper

Com o operador pronto, a conversão de User para UserViewModel acontece em uma simples atribuição:

// Cria uma instância de um usuário
var user = new User
{
    Id = 1,
    Username = "balta",
    Password = "balta1234",
    Active = true
};

// Cria uma instância do ViewModel
// Recebe um usuário (conversão implícita)
UserViewModel viewModel = user;

O caminho inverso também é possível. Para isso, basta implementar outro operador que converta UserViewModel em User.

Em uma API, isso permite retornar o ViewModel a partir da entidade com uma única linha. Assim, o código do endpoint fica limpo e sem dependência de pacotes externos.

Implicit operator em Value Objects de valor único

Muitos Value Objects encapsulam um único valor primitivo, como um e-mail ou um documento. Nesses casos, o caminho do objeto para o primitivo é sempre seguro, enquanto o caminho inverso exige validação.

Por isso, uma combinação comum é usar implicit operator na saída e explicit operator na entrada:

public record Email
{
    public string Address { get; }

    public Email(string address)
    {
        // Validação simples para o exemplo
        if (string.IsNullOrWhiteSpace(address) || !address.Contains('@'))
            throw new ArgumentException("Invalid email address.", nameof(address));

        Address = address;
    }

    // Sempre seguro: todo Email válido é uma string
    public static implicit operator string(Email email) => email.Address;

    // Pode falhar: nem toda string é um e-mail válido
    public static explicit operator Email(string address) => new(address);
}

Com isso, você passa um Email para qualquer método que espera string sem escrever nada extra. Já a criação a partir de texto exige (Email)"user@balta.io", o que deixa claro para o leitor que ali existe uma validação.

Conversões em chamadas de método

As conversões implícitas não acontecem apenas em atribuições. Segundo a documentação, elas também ocorrem em chamadas de método, sempre que o argumento tem um tipo diferente do parâmetro.

Por exemplo, imagine um método que envia SMS e recebe um Phone. Com o operador de string para Phone, você pode passar o texto diretamente:

public static void SendSms(Phone phone, string message)
{
    // Envia a mensagem para o número formatado
    Console.WriteLine($"Sending '{message}' to {phone}");
}

// A string é convertida em Phone automaticamente
SendSms("55 11 999999999", "Your order has shipped");

Nesse caso, o compilador percebe que o primeiro argumento é uma string, mas o parâmetro espera um Phone. Então, ele procura um implicit operator compatível e o aplica antes da chamada.

O mesmo vale para o retorno de métodos. Assim, um método que declara retorno UserViewModel pode simplesmente fazer return user; quando recebe um User.

Essa comodidade é poderosa, mas pede atenção. Quem lê a chamada não vê a conversão, por isso o operador precisa ser previsível e livre de surpresas.

Como o compilador resolve o implicit operator

Por baixo dos panos, um operador de conversão é apenas um método estático. Na compilação, o C# gera um método especial chamado op_Implicit para cada implicit operator e op_Explicit para cada explicit operator.

Então, quando você escreve phone = telephone;, o compilador troca a atribuição por uma chamada a Phone.op_Implicit(telephone). Ou seja, não existe mágica em tempo de execução, nem Reflection.

Esse detalhe explica a boa performance em relação a bibliotecas de mapeamento baseadas em Reflection. Afinal, a conversão vira uma chamada de método comum, resolvida durante a compilação.

Também explica por que a escolha do operador acontece em tempo de compilação. Portanto, se a variável for do tipo object, o compilador não encontra o operador e a conversão não acontece.

Por exemplo, o código abaixo não compila, mesmo que o objeto guardado seja um User:

object data = new User { Username = "balta" };

// Erro: não existe conversão implícita de object para UserViewModel
UserViewModel viewModel = data;

Nesse caso, você precisa primeiro converter data para User. Em seguida, a atribuição ao UserViewModel volta a usar o operador normalmente.

Quando usar implicit operator e quando evitar

O implicit operator brilha quando a conversão é óbvia, segura e sempre bem-sucedida. Value Objects que encapsulam um valor primitivo, como e-mail, CPF ou telefone, são candidatos naturais.

Também funciona bem em ViewModels e DTOs que apenas recortam dados de uma entidade. Nesses casos, a conversão não depende de serviços externos nem de regras complexas.

Por outro lado, evite o operador quando a conversão puder falhar. Nesse cenário, prefira o explicit operator ou um método com nome claro, como FromString ou TryParse.

Evite também conversões que escondem efeitos colaterais, como acesso ao banco ou chamadas HTTP. Afinal, quem lê UserViewModel viewModel = user; espera uma operação barata e previsível.

Outro ponto de atenção são as conversões que cruzam camadas. Se a entidade de domínio declarar um operador para um ViewModel da API, o domínio passa a depender da camada de apresentação, e por isso o operador deve ficar no ViewModel.

Também vale documentar o formato esperado sempre que a conversão partir de texto. Assim, quem usa o tipo sabe exatamente qual string gera um objeto válido.

Por fim, cuidado com o excesso de conversões entre os mesmos tipos. Muitos operadores implícitos deixam o código difícil de seguir, porque o leitor não vê onde cada conversão acontece.

Implicit operator e explicit operator lado a lado

A documentação da Microsoft traz um exemplo que mostra os dois operadores juntos. Nele, uma struct Digit aceita apenas valores de 0 a 9:

public readonly struct Digit
{
    private readonly byte digit;

    public Digit(byte digit)
    {
        if (digit > 9)
            throw new ArgumentOutOfRangeException(nameof(digit), "Digit cannot be greater than nine.");

        this.digit = digit;
    }

    // Sempre seguro: todo Digit cabe em um byte
    public static implicit operator byte(Digit d) => d.digit;

    // Pode falhar: nem todo byte é um Digit válido
    public static explicit operator Digit(byte b) => new Digit(b);
}

A conversão de Digit para byte nunca falha, então ela é implícita. Já a conversão de byte para Digit pode lançar exceção, por isso exige cast.

Esse exemplo resume bem o critério de escolha. Em outras palavras, use o implicit operator quando a conversão for sempre segura e o explicit quando houver risco.

Para ver todos os detalhes, consulte a página oficial sobre operadores de conversão definidos pelo usuário.

Conclusão

O C# é uma linguagem rica, mas muitas vezes deixamos passar recursos que ajudam a reduzir código e o uso de pacotes externos. O implicit operator é um desses recursos.

Com ele, você converte Value Objects em strings, cria objetos a partir de texto e transforma entidades em ViewModels com uma simples atribuição. Além disso, tudo isso acontece sem Reflection e com a conversão visível no próprio tipo.

Portanto, use o implicit operator para conversões seguras e óbvias, e recorra ao explicit quando houver risco de falha. Dessa forma, o seu código fica mais limpo sem abrir mão da previsibilidade.

FAQ

Qual a diferença entre implicit e explicit operator?

O implicit operator converte sem cast, em uma simples atribuição. Já o explicit operator exige o cast, como (Digit)value, e serve para conversões que podem falhar.

Posso criar um implicit operator entre dois tipos do .NET?

Não, porque o operador precisa ser declarado no tipo de origem ou no tipo de destino. Como você não altera os tipos do .NET, a conversão só vale para tipos seus.

O implicit operator funciona com records?

Sim, records aceitam operadores de conversão como classes e structs. O exemplo do telefone usa exatamente um record.

O implicit operator substitui o AutoMapper?

Em muitos cenários simples, sim, porque a conversão fica explícita e sem Reflection. Em mapeamentos muito grandes, avalie se o ganho de produtividade de uma biblioteca compensa.

Os operadores is e as usam o implicit operator?

Não, os operadores is e as ignoram conversões definidas pelo usuário. Apenas atribuições, chamadas de método e casts disparam o operador.

Glossário

Termo Significado
Implicit operator Operador de conversão definido pelo usuário que o compilador aplica sem cast.
Explicit operator Operador de conversão definido pelo usuário que exige cast para ser usado.
Cast Sintaxe que força uma conversão de tipo, como (int)x.
Value Object Objeto definido pelos seus valores, sem identidade própria, como um telefone.
Record Tipo do C# com igualdade por valor e sintaxe concisa para dados.
DTO Objeto usado apenas para transportar dados entre camadas ou sistemas.
ViewModel Objeto que representa os dados exibidos ou retornados para o cliente.
Reflection Recurso do .NET que inspeciona tipos em tempo de execução.

Fontes