Desvendando o Poder das Flags em C# e ReactJS: Por Que Este Simples Truque Pode Mudar Tudo! (#csharp #dev #flags #dotnet #enums #reactjs #javascript #typescript)

Desvendando o Poder das Flags em C# e ReactJS

Overview

Já abriu o código em C# e se deparou com [Flags] e pensou: “Hmm, que diabos é isso?” Você não está sozinho! Hoje, vamos descobrir como esse atributo funciona e como você pode tirar proveito dele para melhorar sua aplicação.

O atributo [Flags] é um recurso novo que foi lançado no C# 10, e… Brincadeira! 😂 Esse recurso está presente desde a versão 1.0.

Ele permite que um tipo enum represente uma combinação de valores. Sim, com uma propriedade, você pode armazenar múltiplos valores. O que significa que, em vez de ter IList<UserPermission>, você pode ter UserPermission e armazenar todas as permissões do usuário em uma única variável que é convertida para um simples integer, que pesa bem menos na memória e é bem mais performático. 🤯

Para verificar e modificar esses valores, você precisará usar operações bitwise. Mas não se preocupe, parece complicado, mas não é.

Uma curiosidade: esse conceito não é exclusivo do C#. Se você já brincou com permissões de arquivos no Linux —aqueles números mágicos como 777, 644 ou 755—você já está familiarizado com a ideia subjacente!

Aqui está como você poderia emular permissões de arquivos no estilo Linux usando Flags em C#:

[Flags]
enum LinuxPermission
{
    None = 0,
    Execute = 1,
    Write = 2,
    Read = 4
}

// 777 permissions would be:
var userPermissions = LinuxPermission.Read | LinuxPermission.Write | LinuxPermission.Execute;
var groupPermissions = LinuxPermission.Read | LinuxPermission.Write | LinuxPermission.Execute;
var otherPermissions = LinuxPermission.Read | LinuxPermission.Write | LinuxPermission.Execute;

Por Que Usar Flags em Vez de uma Lista de Enums?

Imagine gerenciar uma lista de enums apenas para representar seleções múltiplas. Você precisa se preocupar com duplicações e ordenação. Ao verificar um valor específico, você precisa iterar pela lista para encontrá-lo, etc.

Parece pesado, certo? Usar Flags simplifica isso tremendamente:

  • Simplicidade: Um único inteiro armazena várias opções. Eficiente em termos de armazenamento!
  • Desempenho: Operações bitwise são extremamente rápidas em comparação com a iteração por listas.
  • Clareza: Combina opções relacionadas em código claro e conciso.

Quando Usar e Evitar Flags

Use Flags quando:

  • Você precisa de várias opções simultâneas.
  • O desempenho e a eficiência de memória são importantes.
  • Você deseja código claro, expressivo e legível.

Evite Flags quando:

  • Cada valor de enum deve ser estritamente mutuamente exclusivo.
  • Seu enum representa naturalmente estados distintos em vez de opções combináveis.

Obviamente, esta não é uma lista exaustiva e isso não é uma regra rígida. Use seu bom senso e julgamento para decidir quando usar Flags.

Exemplo Prático / Como Usar Flags em C#

Aqui está um exemplo prático para ilustrar como Flags podem simplificar seu código:

```c#
[Flags]
public enum PizzaToppings
{
    None = 0,
    Cheese = 1,
    Pepperoni = 2,
    Olives = 4,
    Mushrooms = 8,
    AllToppings = Cheese | Pepperoni | Olives | Mushrooms
}

class Program
{
    static void Main(string[] args)
    {
        PizzaToppings myPizza = PizzaToppings.Cheese | PizzaToppings.Pepperoni;

        Console.WriteLine($"My pizza toppings: {myPizza}");

        // Check if specific topping is selected
        bool hasOlives = myPizza.HasFlag(PizzaToppings.Olives);
        Console.WriteLine($"Does my pizza have olives? {hasOlives}");

        // Adding toppings
        myPizza |= PizzaToppings.Olives;
        Console.WriteLine($"Updated pizza toppings: {myPizza}");

        // Removing toppings
        myPizza &= ~PizzaToppings.Cheese;
        Console.WriteLine($"Final pizza toppings: {myPizza}");
    }
}

Como Usar Flags em ReactJS

Tudo o que dissemos até agora é apenas para C#, e é ótimo e tudo mais, mas como usamos isso em ReactJS? Afinal, no React não tem atributos como no C#. 🤔 Felizmente, também é algo bem simples.

Considere os seguintes models em C#:

[Flags]
public enum UserPermissions
{
  Read = 1 << 0,
  Write = 1 << 1,
  DirectMessage = 1 << 2,
  CreateGroup = 1 << 3,
  InviteToGroup = 1 << 4,
  KickFromGroup = 1 << 5,
  BanFromGroup = 1 << 6,
  ModerateGroupMessages = 1 << 7,
  PromoteToAdmin = 1 << 8,
  GroupAdmin = InviteToGroup | KickFromGroup | BanFromGroup | ModerateGroupMessages,
  GroupOwner = GroupAdmin | PromoteToAdmin,
}

[Flags]
public enum UserRoles
{
  Anonymous = 1 << 0,
  PreMember = 1 << 1,
  Member = 1 << 2,
  SubscriberTier1 = 1 << 3,
  SubscriberTier2 = 1 << 4,
  SubscriberTier3 = 1 << 5,
  Admin = 1 << 6,
}

public class User
{
  public Guid Id { get; set; }
  public string Name { get; set; } = string.Empty;
  public string Email { get; set; } = string.Empty;
  public UserRoles Roles { get; set; }
  public UserPermissions Permissions { get; set; }
  public DateTime CreatedAt { get; set; }
  public DateTime UpdatedAt { get; set; }
  public DateTime LastLogin { get; set; }
}

No código acima, temos um modelo super simplista para um usuário, com funções e permissões.

Agora, vamos ver como você pode fazer as mesmas operações em ReactJS. Vamos supor que você tenha um ReactJS para esta aplicação. Este seria o modelo equivalente em TypeScript:

export enum UserRoles {
    Anonymous = 1 << 0,
    PreMember = 1 << 1,
    Member = 1 << 2,
    SubscriberTier1 = 1 << 3,
    SubscriberTier2 = 1 << 4,
    SubscriberTier3 = 1 << 5,
    Admin = 1 << 6,
}

export enum UserPermissions {
    Read = 1 << 0,
    Write = 1 << 1,
    DirectMessage = 1 << 2,
    CreateGroup = 1 << 3,
    InviteToGroup = 1 << 4,
    KickFromGroup = 1 << 5,
    BanFromGroup = 1 << 6,
    ModerateGroupMessages = 1 << 7,
    PromoteToAdmin = 1 << 8,
    GroupAdmin = InviteToGroup | KickFromGroup | BanFromGroup | ModerateGroupMessages,
    GroupOwner = GroupAdmin | PromoteToAdmin,
}

export interface User {
    id: string;
    name: string;
    email: string;
    roles: UserRoles;
    permissions: UserPermissions;
    createdAt: string;
    updatedAt: string;
    lastLogin: string;
}

Como você pode ver, os modelos são praticamente os mesmos nas duas linguagens. Você pode copiar os modelos de C# e colá-los em TypeScript.

E para fazer as mesmas operações que fizemos em C#, você pode fazer o seguinte:

// Check if user has a specific permission
const hasFlag = (value: number, flag: number): boolean => (value & flag) === flag;

// Example:
const user: User = {
    id: '123',
    name: 'John Doe',
    email: 'foo@bar.today',
    roles: UserRoles.Admin,
    permissions: UserPermissions.Read | UserPermissions.Write,
    createdAt: '2022-01-01',
    updatedAt: '2022-01-01',
    lastLogin: '2022-01-01',
}

//Check if user is Admin
const isAdmin = hasFlag(user.roles, UserRoles.Admin); //Will return true.

E é isso! Agora você pode usar Flags em ReactJS também. 🎉 Claro, o exemplo acima é super simplista, mas você pode expandi-lo conforme necessário. Se você quiser brincar com isso, na verdade criei o Backend e o Frontend completo para este exemplo. Está disponível neste repositório no meu GitHub.

Neste repositório, você encontrará:

  • Uma API C# Minimal simples que usa os modelos acima e salva os dados em um banco de dados SQLite, usando EFCore. Mesmo sendo um exagero total para isso, eu queria mostrar como esse recurso se integra perfeitamente com o EFCore.
  • Um frontend ReactJS simples que usa os modelos acima e mostra os dados em uma tabela. Você poderá listar, adicionar, editar e excluir usuários.

Recomendo abrir as Devtools no seu navegador e verificar a guia de rede para ver as solicitações feitas e os valores enviados e recebidos nos enums UserPermissions e UserRoles.

Observações sobre o Método HasFlag

In the C# example above, I used myPizza.HasFlag(PizzaToppings.Olives); to check if myPizza has the Olives topping. The HasFlag method is a built-in method that does some validation before checking the flag, this means that this is tad slower than the bitwise operation (myPizza & PizzaToppings.Olives) == PizzaToppings.Olives. You gain a lot in terms of readability, but you lose a lot in term os performance.

Diferenças de performance

Neste post, falei bastante sobre as flags e como elas são mais rápidas que as listas, mas o quão mais rápidas são elas? Hora de mostrar algumas evidências.

Criei um benchmark entre algumas operações usando listas e flags. Os resultados estão abaixo: (Você pode encontrar este projeto de benchmark no repositório mencionado acima)

Lembrete: 1 Nanosecond == 0.000000001 Seconds

Operações com Lista

MethodMeanErrorStdDevStdErrMedianMinMaxAllocated
AddUserPermission50.00 ns17.71 ns52.22 ns5.222 ns0.0000 ns0.0000 ns200.0 ns400 B
RemoveUserPermission341.24 ns17.06 ns49.48 ns5.024 ns300.0000 ns300.0000 ns400.0 ns400 B
CheckIfUserPermissionExistsUsingContains277.65 ns15.50 ns41.91 ns4.546 ns300.0000 ns200.0000 ns300.0 ns400 B
CheckIfUserPermissionExistsUsingAny1,523.47 ns50.87 ns148.39 ns14.990 ns1,500.0000 ns1,300.0000 ns1,900.0 ns440 B
CheckIfUserPermissionExistsUsingFind454.64 ns26.80 ns77.76 ns7.895 ns400.0000 ns300.0000 ns600.0 ns400 B
CheckIfUserPermissionExistsUsingFirstOrDefault1,459.02 ns31.72 ns71.59 ns9.167 ns1,500.0000 ns1,300.0000 ns1,600.0 ns440 B

Como o nome dos métodos sugere, aqui está o que eles estão fazendo:

  1. AddUserPermission: Adiciona uma permissão a uma lista usando o método Add.
  2. RemoveUserPermission: Remove uma permissão de uma lista usando o método Remove.
  3. CheckIfUserPermissionExistsUsingContains: Verifica se uma permissão existe em uma lista usando o método Contains.
  4. CheckIfUserPermissionExistsUsingAny: Verifica se uma permissão existe em uma lista usando o método Any.
  5. CheckIfUserPermissionExistsUsingFind: Verifica se uma permissão existe em uma lista usando o método Find.
  6. CheckIfUserPermissionExistsUsingFirstOrDefault: Verifica se uma permissão existe em uma lista usando o método FirstOrDefault.

Dos métodos para verificar se um valor existe dentro de uma lista, o método Contains é o mais rápido dos métodos de lista. Agora, vamos ver os resultados para as operações de flags:

Operações com Flags

MethodMeanErrorStdDevStdErrMedianMinMaxAllocated
AddUserPermission0.0000 ns0.0000 ns0.0000 ns0.0000 ns0.0000 ns0.0000 ns0.0000 ns400 B
RemoveUserPermission0.0000 ns0.0000 ns0.0000 ns0.0000 ns0.0000 ns0.0000 ns0.0000 ns400 B
CheckIfUserPermissionExistsUsingHasFlag645.1613 ns24.6623 ns69.9629 ns7.2548 ns600.0000 ns500.0000 ns800.0000 ns448 B
CheckIfUserPermissionExistsUsingBitwise34.3434 ns16.9868 ns49.8193 ns5.0070 ns0.0000 ns0.0000 ns200.0000 ns400 B
CheckIfUserPermissionExistsUsingBitwiseWrapper50.0000 ns18.9784 ns55.9581 ns5.5958 ns0.0000 ns0.0000 ns200.0000 ns400 B

Os métodos são:

  1. AddUserPermission: Adiciona uma permissão a uma flag usando o operador |.
  2. RemoveUserPermission: Remove uma permissão de uma flag usando o operador &.
  3. CheckIfUserPermissionExistsUsingHasFlag: Verifica se uma permissão existe em uma flag usando o método HasFlag.
  4. CheckIfUserPermissionExistsUsingBitwise: Verifica se uma permissão existe em uma flag usando a operação bitwise (Exemplo (myPizza & PizzaToppings.Olives) == PizzaToppings.Olives).
  5. CheckIfUserPermissionExistsUsingBitwiseWrapper: Verifica se uma permissão existe em uma flag usando uma função de wrapper que faz a operação bitwise. Adicionei isso porque é uma maneira simples de melhorar a legibilidade mantendo o desempenho.

Como você pode ver, a operação bitwise para verificar se o usuário tem permissão é a mais rápida. Usar o método auxiliar nativo HasFlag é decepcionantemente lento em comparação com a operação bitwise.

Comparando a operação mais rápida das operações de lista com a mais rápida das operações de flags, as flags são ~8 vezes mais rápidas. Mesmo usando a verificação com a função de wrapper, as flags ainda são ~5 vezes mais rápidas. Se olharmos par ao valor Mediano para essa comparação, a vantagem de desempenho das flags é ainda mais evidente.

Conclusão

Então, você deve usá-lo ou não? Os benefícios de desempenho sozinhos são suficientes? Bem, depende. As flags são ótimas, mas reduzem a legibilidade do código. À primeira vista, os valores seriam um número inteiro aleatório, e isso pode ser confuso se você quiser consultá-los em um banco de dados.

Você pode criar as flags em seu sistema de forma que os números sejam mais significativos, como UserPermissions = 4 significa que o usuário pode Ler e Escrever, e se for igual a 777, significa que o usuário é um Admin ou algo assim. Existem maneiras de mitigar o problema de legibilidade, mas é algo a ser considerado bem antes de implementá-lo.

Se em seu sistema você tem uma propriedade que pode conter múltiplos valores ao mesmo tempo, você precisa direcionar o fluxo do código dependendo desses valores e desempenho é uma parte crucial, então as Flags provavelmente são o caminho a seguir. Lembre-se: A legibilidade (o quão fácil é para um dev ler o código) é legal e conveniente para o desenvolvedor durante um período em que você está debugando, mas você deve ter cuidado ao escolher sacrificar a simplicidade e o desempenho do sistema apenas para ser mais conveniente para você escrever uma consulta SQL nas (espero que raras) ocasiões em que você precisa verificar o banco de dados diretamente.

Vamos dizer que você está processando pagamentos com cartão de crédito em tempo real e antes de prosseguir com cada transação, você precisa verificar os benefícios do cartão, para que o código saiba o que fazer com a transação. Nesse caso, como cada milissegundo conta, as flags podem ser uma ótima escolha.

Por outro lado, mesmo que o desempenho seja importante, se você tiver uma API e ela não for uma de alta frequência (como milhares de solicitações por segundo), é possível que os benefícios de desempenho das flags não façam muita diferença.

Espero ter ajudado!

References:

Traduções: