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)

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
Method | Mean | Error | StdDev | StdErr | Median | Min | Max | Allocated |
---|---|---|---|---|---|---|---|---|
AddUserPermission | 50.00 ns | 17.71 ns | 52.22 ns | 5.222 ns | 0.0000 ns | 0.0000 ns | 200.0 ns | 400 B |
RemoveUserPermission | 341.24 ns | 17.06 ns | 49.48 ns | 5.024 ns | 300.0000 ns | 300.0000 ns | 400.0 ns | 400 B |
CheckIfUserPermissionExistsUsingContains | 277.65 ns | 15.50 ns | 41.91 ns | 4.546 ns | 300.0000 ns | 200.0000 ns | 300.0 ns | 400 B |
CheckIfUserPermissionExistsUsingAny | 1,523.47 ns | 50.87 ns | 148.39 ns | 14.990 ns | 1,500.0000 ns | 1,300.0000 ns | 1,900.0 ns | 440 B |
CheckIfUserPermissionExistsUsingFind | 454.64 ns | 26.80 ns | 77.76 ns | 7.895 ns | 400.0000 ns | 300.0000 ns | 600.0 ns | 400 B |
CheckIfUserPermissionExistsUsingFirstOrDefault | 1,459.02 ns | 31.72 ns | 71.59 ns | 9.167 ns | 1,500.0000 ns | 1,300.0000 ns | 1,600.0 ns | 440 B |
Como o nome dos métodos sugere, aqui está o que eles estão fazendo:
- AddUserPermission: Adiciona uma permissão a uma lista usando o método
Add
. - RemoveUserPermission: Remove uma permissão de uma lista usando o método
Remove
. - CheckIfUserPermissionExistsUsingContains: Verifica se uma permissão existe em uma lista usando o método
Contains
. - CheckIfUserPermissionExistsUsingAny: Verifica se uma permissão existe em uma lista usando o método
Any
. - CheckIfUserPermissionExistsUsingFind: Verifica se uma permissão existe em uma lista usando o método
Find
. - 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
Method | Mean | Error | StdDev | StdErr | Median | Min | Max | Allocated |
---|---|---|---|---|---|---|---|---|
AddUserPermission | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 400 B |
RemoveUserPermission | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 400 B |
CheckIfUserPermissionExistsUsingHasFlag | 645.1613 ns | 24.6623 ns | 69.9629 ns | 7.2548 ns | 600.0000 ns | 500.0000 ns | 800.0000 ns | 448 B |
CheckIfUserPermissionExistsUsingBitwise | 34.3434 ns | 16.9868 ns | 49.8193 ns | 5.0070 ns | 0.0000 ns | 0.0000 ns | 200.0000 ns | 400 B |
CheckIfUserPermissionExistsUsingBitwiseWrapper | 50.0000 ns | 18.9784 ns | 55.9581 ns | 5.5958 ns | 0.0000 ns | 0.0000 ns | 200.0000 ns | 400 B |
Os métodos são:
- AddUserPermission: Adiciona uma permissão a uma flag usando o operador
|
. - RemoveUserPermission: Remove uma permissão de uma flag usando o operador
&
. - CheckIfUserPermissionExistsUsingHasFlag: Verifica se uma permissão existe em uma flag usando o método
HasFlag
. - CheckIfUserPermissionExistsUsingBitwise: Verifica se uma permissão existe em uma flag usando a operação bitwise (Exemplo
(myPizza & PizzaToppings.Olives) == PizzaToppings.Olives
). - 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: