AlgoMaster Logo

[Flags] Attribute

Last Updated: May 17, 2026

16 min read

A regular enum lets a variable hold one value at a time. The [Flags] attribute changes the contract: each value of the enum becomes a single bit, and a variable can hold any combination of those bits inside one integer. This is how you model a set of independent on/off options (notification channels, shipping add-ons, product badges) without paying for a separate boolean field per option, and without losing the type safety that an enum gives you. This lesson focuses on what changes when the values are meant to be combined.

Why Flags Exist

Imagine a customer profile that tracks which notification channels the customer has opted into. A naive design would use four bool fields:

This works, but it has a few quiet problems. The four booleans aren't named as a group, so nothing in the type system says "these belong together." Passing the whole set to a method means passing four parameters or a whole CustomerPreferences object. Storing the set in a database row takes four columns. Adding a fifth channel later means changing every signature, every column, every constructor.

A [Flags] enum collapses all of that into a single value:

One integer (four bits used) carries the same information as four bool fields. The type system knows the value belongs to NotificationChannels. The pretty ToString() prints "Email, Push" instead of a number. Adding a fifth channel later is a one-line change in the enum, and no method signatures need to change.

The [Flags] attribute itself does almost nothing at runtime. It's a marker that mainly affects two things:

  • ToString() and Enum.Format("G") recognize that a non-named combined value should be printed as a comma-separated list of the matching flag names.
  • Tools, documentation, and analyzers know to treat the enum as a bit set rather than an exclusive choice.

Everything else (the |, &, ^, ~ operators, the integer storage) works on any enum regardless of the attribute. The attribute is the discipline; the bitwise operators are the mechanics. Both have to be in place for the design to work cleanly.

The Naming and Value Conventions

A flags enum has three conventions that you should follow every time, because every consumer of the type assumes them.

Plural name. The enum represents a set of values, not a single value. Use Permissions, not Permission. Use NotificationChannels, not NotificationChannel. Use ShippingOptions, not ShippingOption. A reader who sees the plural name on a variable understands at a glance that the variable may hold more than one thing.

Always include `None = 0`. Zero is the "no flags set" value, and it has special status. It's the default value of any enum field (a freshly initialized struct or class gets all zeros), and flags & anything == 0 is how you check "no bits in common." Giving zero a name makes that default state readable and gives HasFlag a meaningful identity behavior. Skipping None doesn't break the code, but every reader has to mentally invent the name, and default(NotificationChannels) prints as 0 instead of None.

Powers of two for individual flags. Every named single-flag value must be 1, 2, 4, 8, 16, 32, ..., that is, a value with exactly one bit set. This is what makes combinations unambiguous: each bit position belongs to one flag, and combining two flags with | cannot accidentally collide with a third flag's value. The two common styles for writing these values are the literal style and the shift style:

Both compile to identical metadata. The literal style is shorter and reads well for up to about eight flags. The shift style is harder to typo (you increment the shift count by one each line, no doubling math required) and scales cleanly to 32 flags without a calculator. Pick one per enum and stick with it.

You can also define combined named values for combinations that come up often. These are not new flags, they're aliases for a | of existing flags:

ReadWrite is the same value as Read | Write. Code that returns Permissions.ReadWrite is identical at runtime to code that returns Permissions.Read | Permissions.Write. The combined names exist for readability at call sites: RequirePermissions(Permissions.ReadWrite) reads better than RequirePermissions(Permissions.Read | Permissions.Write) when the combination is common.

Visualizing Bit Composition

A [Flags] enum value is just an integer where each bit corresponds to a named flag. Looking at the binary representation makes the combination behavior obvious.

Consider Permissions.ReadWrite, which is Permissions.Read | Permissions.Write:

The OR operation sets every bit that was set in either input. Read had bit 0 set; Write had bit 1 set; the result has both. Adding Delete (which is 4, or bit 2) to the same value would produce 7 (0000 0111). Adding Admin (8, bit 3) on top would produce 15 (0000 1111), which is the value of Permissions.All defined earlier.

The same picture for NotificationChannels.Email | NotificationChannels.Push:

Bit 0 (Email) and bit 2 (Push) are set. Bit 1 (Sms) and bit 3 (InApp) are clear. A different combination would light up a different pair of bits. Because each flag owns a unique bit, the integer carries the full membership information of the set in log2(N) bits, where N is the value of the largest flag.

This is also why the powers-of-two rule isn't a stylistic choice: if Sms were 3 (bits 0 and 1 set) instead of 2, then Email | Sms would be 0000 0011, which is also 3, which is also the bit pattern of Sms alone, which is also the bit pattern of Email | (something with both bits 0 and 1). There's no way to ask "is Sms in here?" without ambiguity. The bit-per-flag rule is what makes membership tests work at all.

The Four Operations: Set, Clear, Toggle, Check

Every flags enum is manipulated with four bitwise operations. The integer math is the same regardless of the enum type, so the patterns transfer to any [Flags] enum you encounter.

Each operation has a different bitwise identity, and it helps to read them as English first and code second:

  • Set (flags |= X): turn the bits of X on, leave everything else alone. OR is the right operator because bit | 1 = 1 and bit | 0 = bit.
  • Clear (flags &= ~X): turn the bits of X off, leave everything else alone. ~X flips every bit of X (so the bits of X become 0 and everything else becomes 1), and AND with that mask zeroes only the bits of X.
  • Toggle (flags ^= X): flip the bits of X. XOR returns 1 when exactly one of the inputs is 1, so XOR-ing with X flips wherever X has a 1 and leaves wherever X has a 0.
  • Check ((flags & X) != 0): isolate the bits of X from flags. If any bit of X is also set in flags, the result is non-zero.

The check pattern has an edge case worth knowing about: when X is a multi-bit combination like Permissions.ReadWrite, the != 0 test returns true if any of the requested bits are set, not all of them. To require all bits, compare against X itself:

The expression (p & ReadWrite) == ReadWrite says "after masking down to just the Read and Write bits, are both of them still on?" This is the right check for "do you have all of these permissions?" The != 0 variant answers "do you have any of these permissions?" Most of the time you want the all-bits version, especially for combined named values like ReadWrite or All.

The same four operations cover essentially every flag-manipulation task you'll write. Granting a permission is a set. Revoking is a clear. Toggling a feature on/off is a toggle. Checking authorization is a check. If you find yourself reaching for a custom helper that does something more elaborate, the chances are you're modeling state that shouldn't be a single flags value, and the right move is to break it into two or more enums.

HasFlag and Its Performance Story

C# has had a built-in helper for the check pattern since .NET Framework 4.0: Enum.HasFlag. It reads cleaner than the bitwise expression for most code.

HasFlag(X) is equivalent to (p & X) == X, which means it does the all-bits-required check that you usually want. Passing a combined value like Permissions.ReadWrite correctly reports whether both Read and Write are set, not "at least one of them."

The historical wart with HasFlag is performance. In .NET Framework and early .NET Core, the implementation was a generic method that took the flag as a boxed Enum, which meant every call allocated. On a tight loop that checked flags millions of times, the garbage collection pressure was measurable, and the standard advice was to avoid HasFlag in performance-sensitive code and write (flags & X) != 0 by hand.

Starting in .NET Core 2.1, the JIT compiler started recognizing HasFlag calls on enum types and replacing them with the equivalent bitwise expression at the call site, removing the boxing entirely. From .NET 6 onward, this optimization is reliable across configurations, so p.HasFlag(Permissions.Read) and (p & Permissions.Read) == Permissions.Read compile to effectively the same code. The Microsoft documentation for Enum.HasFlag describes this optimization explicitly.

The practical guidance: if you're on .NET 6 or newer, use HasFlag freely. It's clearer at the call site, and the JIT does the work to make it cheap. If you're maintaining a codebase that targets older runtimes, or you're writing a library that has to work on .NET Framework, sticking to the explicit bitwise form is still the safer choice. The result is the same, only the legibility differs.

One thing HasFlag doesn't give you is the "any bit" check. p.HasFlag(X) requires every bit of X to be set. There's no built-in HasAnyFlag, so for the "at least one of these channels is enabled" question you still need the bitwise form (p & X) != 0.

ToString() and How Flags Print

The [Flags] attribute mainly earns its keep through ToString(). Without the attribute, an enum value that doesn't exactly match a named member prints as a number:

The value 5 doesn't match any named member of the plain enum, so ToString() falls back to printing the underlying number. The reader has to know the enum well enough to translate 5 back into "Email + Push" in their head.

With [Flags], the runtime recognizes that 5 is a valid combination of named bits and prints the names:

The decomposition runs from the largest declared flag to the smallest, picking off bits that match named members. If a leftover bit doesn't correspond to any named flag, the whole value falls back to a number. So if you cast (NotificationChannels)20 (bit 4 plus bit 2, but bit 4 isn't named), you get "20" back, not "Push, 16".

A subtle quirk: if a combined named value matches exactly, the combined name wins over the constituent names.

For p, the value is exactly 3, which matches the named member ReadWrite, so that name is used. For q, the value is 7. The decomposition finds that 7 = ReadWrite | Delete, and since ReadWrite is a larger named value than the alternative Read | Write | Delete triple, it gets picked first. This is mostly a cosmetic detail, but it explains why log output sometimes shows combined names that you didn't explicitly construct.

Logging, debugging output, and error messages all flow through ToString(), so this is the main user-visible benefit of the attribute. A log line that reads "Permissions: ReadWrite, Delete" is immediately useful. A log line that reads "Permissions: 7" requires the on-call engineer to grep the enum definition.

When Things Go Wrong: Two Failure Modes

The two most common ways to break a [Flags] enum are choosing non-power-of-two values and trusting Enum.IsDefined. Both are easy to do by accident, and both produce code that looks reasonable but behaves incorrectly.

What's wrong with this code?

The values 1, 2, 3, 4 look like a natural sequence, but 3 is not a power of two. The binary patterns are Email = 0001, Sms = 0010, Push = 0011, InApp = 0100. The bit pattern of Push is exactly the union of Email and Sms. So when you compute Email | Sms, you get 0011, which the runtime can't distinguish from Push. ToString() prints "Push" even though the user only opted into email and SMS, and HasFlag(Push) returns true for the same reason. Worse, InApp = 4 is fine on its own, but Email | Push is 0001 | 0011 = 0011, the same Push, swallowing the Email part entirely.

Fix: Use powers of two for individual flags. The "third channel" gets value 4, the "fourth channel" gets value 8, and so on:

Once each flag has a unique bit, combinations are unambiguous. Email | Sms is 3, which has no named match (so ToString() prints "Email, Sms"), and HasFlag(Push) correctly returns false.

What's wrong with this code?

The value Read | Write is 3. The enum doesn't declare a named member with the value 3, so Enum.IsDefined returns false. From the caller's point of view, this is misleading: Read | Write is obviously a valid combination of declared flags, and ToString() happily prints "Read, Write". The check that's supposed to mean "is this a legal value for the enum?" answers "no" for legal flag combinations.

The root issue is that Enum.IsDefined was designed for plain enums, where each value of the variable is supposed to match one of the named members. For a [Flags] enum, the set of valid values is the power set of the named flags (every combination), not just the named ones. The method has no concept of bit composition, so it can't tell the difference between a valid combination and an invalid one.

Fix: Validate flags values by masking against the union of every defined flag. If the masked value equals the original, every set bit is accounted for:

Wait, the output above shows four lines because the example actually has three checks and a printed True/False per line. Read top to bottom:

  • IsValid(Permissions.Read | Permissions.Write) prints True because 3 masked against ~All is zero.
  • IsValid((Permissions)16) prints False because bit 4 isn't part of any declared flag.
  • IsValid(Permissions.Read | Permissions.Delete) prints True because both bits are declared.

The mask trick is the standard way to validate flags values from untrusted input (a parsed config file, a JSON payload, an integer from a database). If you genuinely need this check often, declare an All member in the enum and reuse it as the mask:

Then validation is (p & ~Permissions.All) == 0, which reads better and updates automatically when you add a new flag.

E-Commerce Examples in Practice

The same patterns show up across the kind of code you actually write for an online store. Three concrete examples make the patterns less abstract.

Notification Channels

A customer's communication preferences naturally fit a flags enum. The customer either receives email or doesn't, either receives SMS or doesn't, and so on. The choices are independent, and any combination is meaningful.

The class wraps the flags value with three semantic methods (Enable, Disable, Has) so callers don't have to remember the bitwise idioms. Internally, every method is one line, and the underlying state is a single int-sized field.

Shipping Options

A shipping selection at checkout often combines several add-ons. The customer might want express delivery with insurance and a signature on delivery. Or standard shipping with gift wrap. The base shipping speed is a different concept from the add-ons, so it could go in a separate enum, but it can also coexist in one flags enum as long as the speeds are mutually exclusive options inside the same value.

The pricing loop checks each flag and adds its surcharge. Adding a new add-on (PrioritySupport, say) is two lines: a new value in the enum and a new if in the price calculation. Removing one is similar. The structure is open to extension without rewriting the caller.

Note the modeling subtlety: Standard and Express are mutually exclusive (a package can't be both), but the flags enum doesn't enforce that. If you set both bits, nothing in the type system complains. The class that owns the value has to enforce the rule itself, or you split the design: keep a separate ShippingSpeed enum (plain, not flags) for Standard/Express/Overnight and a ShippingAddOns flags enum for the rest. The split is cleaner when the speeds grow beyond two; the combined form is fine when there are only a handful and the validation is centralized.

Product Attributes

Product listings often carry independent badges: on sale, new arrival, best seller, clearance. A product can have any combination of these.

The BadgeLine method leans on ToString() to produce the comma-separated names, with a special case for the empty set. This pattern (use the enum's ToString() directly, override it only when the empty case needs custom wording) is common enough to be worth recognizing.

When Not to Use Flags

[Flags] is the right tool when the values are independent and any combination is meaningful. It's the wrong tool when the values are mutually exclusive.

A regular enum models mutually exclusive options. An order status is exactly one of Placed, Confirmed, Shipped, Delivered, or Cancelled at any given time. A shipping speed is exactly one of Standard, Express, or Overnight. A user role in a simple system is exactly one of Customer, Seller, or Admin. These are choices, not sets.

If you slap [Flags] on this, you allow nonsense values like OrderStatus.Shipped | OrderStatus.Cancelled to compile and pass type-checking. The enum no longer documents "an order is in exactly one state at a time." The downstream code has to handle (or detect and reject) combinations that should never have existed in the first place.

A useful heuristic: ask "can two of these values be true at the same time for the same thing?" If yes, flags. If no, plain enum. A product can be OnSale and NewArrival simultaneously, so flags fit. An order cannot be Shipped and Cancelled simultaneously, so a plain enum fits. The decision is about the domain, not the language feature.

The other smell is when the "flags" of an enum aren't really independent because most combinations are invalid. If you have five flags but only three of the possible 32 combinations make sense, you're probably modeling something that should be a smaller plain enum (with values for each legal combination) plus optional extras for the genuinely independent bits. The flags design assumes the cross-product is meaningful; if it isn't, the design lies.

ScenarioBest fitWhy
Order status (Placed, Shipped, Delivered, ...)Plain enumExactly one at a time
Notification channels (Email, Sms, Push, InApp)[Flags] enumAny combination valid
Shipping speed (Standard, Express, Overnight)Plain enumMutually exclusive
Shipping add-ons (Insured, SignatureRequired, GiftWrap)[Flags] enumIndependent options
Product category (Electronics, Books, Clothing)Plain enumOne primary category
Product badges (OnSale, NewArrival, BestSeller)[Flags] enumMultiple can apply
Days of week a shop is open[Flags] enumAny subset is meaningful
Payment methodPlain enumPick one per transaction

The split is rarely subtle once you ask the cross-product question. When you do find a borderline case (something like "delivery slot," where one customer is Morning and the other is Morning | Afternoon), the answer is usually that there are two different concepts in play and you've been trying to fit them into one type.

Summary

  • The [Flags] attribute marks an enum as a bit set rather than an exclusive choice. Its main runtime effect is making ToString() print combined values as a comma-separated list of flag names.
  • Follow three conventions every time: plural type name (Permissions, ShippingOptions), include None = 0, and assign powers of two to individual flags using either literal values (1, 2, 4, 8) or shift expressions (1 << 0, 1 << 1).
  • Combined named values (ReadWrite = Read | Write, All = Read | Write | Delete | Admin) are aliases for common combinations. They improve readability at call sites and double as validation masks.
  • The four core operations are set (flags |= X), clear (flags &= ~X), toggle (flags ^= X), and check ((flags & X) != 0 for any-bit, (flags & X) == X or flags.HasFlag(X) for all-bits).
  • Enum.HasFlag boxed both arguments before .NET 6 and was slower than the inline bitwise form. On .NET 6 and later, the JIT eliminates the boxing and HasFlag runs at the same speed as (p & X) == X. Use it freely on modern runtimes.
  • Two common pitfalls: assigning non-power-of-two values to individual flags (which causes bit collisions and wrong ToString() output), and trusting Enum.IsDefined to validate flags values (which only matches declared names, not combinations). Validate with (value & ~All) == 0 instead.
  • Use [Flags] when the values are independent and any combination is meaningful. Use a plain enum when the values are mutually exclusive. The deciding question is whether two values can be true for the same thing at the same time.