열거형과 비트 플래그

이정현
∙∙조회 5

20261027 열거형과 비트 플래그

개요

열거형은 값에 이름을 붙여 하나의 타입으로 만드는 문법임. 여러 상태를 한 변수에 담아야 하는 경우가 있음(예를들어 하나의 캐릭터에 걸려있는 상태이상이 화상, 출혈, 독, 슬로우 스턴등 여러개가 걸린다던가 하는 경우). 이때 쓰는 것이 비트 플래그이고, 비트를 다루려면 2진법과 비트 연산자를 알아야 함.

Mermaid
렌더링 중...

열거형

숫자로 상태를 나타내면

난이도나 상태를 숫자로 나타내면 몇가지 문제가 생긴다. 아래와 같은 상황이 있다고 해보자

csharp
// 숫자로 상태를 나타내는 예시
int difficulty = 2;      // 0: 쉬움, 1: 보통, 2: 어려움

if (difficulty == 2)
{
    Console.WriteLine("적의 체력이 두 배임");
}

difficulty = 7;          // 없는 난이도인데 아무 일도 일어나지 않음
Console.WriteLine($"난이도 {difficulty}");

다음과 같은 문제가 있다.

문제내용
읽을 수 없음difficulty == 2가 무슨 뜻인지 주석 없이는 알 수 없음
막아 주지 않음7 을 넣어도 컴파일러가 아무 말도 하지 않음
출력이 숫자임화면에 7이 찍혀도 그것이 무엇인지 알 수 없음

이럴때 사용하는 것이 열거형이다.

enum키워드

열거형은 값에 이름을 붙여서 새로운 타입을 만듦.

csharp
// 열거형 사용 예시
Difficulty difficulty = Difficulty.Hard;

if (difficulty == Difficulty.Hard)
{
    Console.WriteLine("적의 체력이 두 배임");
}

Console.WriteLine($"난이도 {difficulty}");

enum Difficulty
{
    Easy,
    Normal,
    Hard
}

이와 같이 사용하면 조건식이 쉽게 읽히고, 출력에 이름이 나오고, 선언되지 않은 항목 이름을 쓰면 컴파일 오류임. 숫자를 그대로 넣는 것도 막힘. 단 정수를 캐스팅해 선언되지 않은 숫자 값을 담는 것까지 막지는 않음.

항목의 값

열거형 항목은 안쪽에서 정수로 저장됨. 아무것도 적지 않으면 위에서부터 0, 1, 2… 순서로 붙음. 위의 열거형 사용 예시를 참고하면 Difficulty의 Easy는 0, Normal은 1, Hard는 2임.

값을 직접 지정하기

=로 값을 정할 수 있음. 지정하지 않은 항목은 바로 위 항목 + 1 이 됨.

csharp
// 값을 직접 지정한 예시
enum Rarity
{
    Common = 10,
    Rare,             // 11 — 바로 위 + 1
    Epic = 50,
    Legendary         // 51
}

값을 지정하는 이유는 대개 둘임. 저장한 데이터의 숫자와 맞춰야 할 때, 그리고 나중에 항목을 끼워 넣어도 기존 값이 밀리지 않게 하고 싶을 때임.

기반 타입 바꾸기

열거형은 기본적으로 int에 담김. 콜론 뒤에 정수 타입을 적어 바꿀 수 있음.

csharp
// 정수 외의 다른 타입 사용 예시
enum Direction : byte
{
    Up,
    Down,
    Left,
    Right
}

enum BigCode : long
{
    Something
}

쓸 수 있는 타입은 byte, sbyte, short, ushort, int, uint, long, ulong임. 항목이 몇 개뿐이면 byte로 줄여 메모리를 아낄 수 있지만, 대부분은 기본값 int로 둠. 물론 어지간하면 기본값인 int로 사용하는 것이 좋다.

switch와 함께 쓰기

열거형은 switch와 짝이 잘 맞음. 값이 정해져 있으므로 갈래를 빠뜨리지 않고 적을 수 있음. switch식으로도 사용 가능하다.

csharp
// switch + enum 사용 예시
void Describe(Suit suit)
{
    switch (suit)
    {
        case Suit.Spade:
            Console.WriteLine("스페이드 — 검정");
            break;
        case Suit.Heart:
            Console.WriteLine("하트 — 빨강");
            break;
        case Suit.Diamond:
            Console.WriteLine("다이아몬드 — 빨강");
            break;
        case Suit.Club:
            Console.WriteLine("클럽 — 검정");
            break;
    }
}

// switch식 + enum 사용 예시
string GetColor(Suit suit) => suit switch
{
    Suit.Spade => "검정",
    Suit.Club => "검정",
    Suit.Heart => "빨강",
    Suit.Diamond => "빨강",
    _ => "알 수 없음"
};

enum Suit
{
    Spade,
    Heart,
    Diamond,
    Club
}

정수와의 변환

열거형과 정수 사이의 변환에는 괄호 표기가 필요함. 실수로 섞이는 것을 막기 위해서임. 단 예외가 하나 있음. 0은 표기 없이 대입됨. 이 때문에 열거형에 값 0인 항목을 “없음”이나 “기본값”에 해당하는 것으로 두는 편이 좋음. 필드나 배열 요소가 초기화될 때 0이 들어가기 때문임. 첫 항목에 다른 값을 지정할 수도 있으므로 첫 항목이 항상 0인 것은 아님.

정의되지 않은값

정수를 열거형으로 캐스팅하는 것은 값이 목록에 없어도 통과함. 컴파일 오류도, 실행 오류도 나지 않음. 이름을 찾을 수 없으니 열거형으로 형변환을 해도 출력하면 숫자가 그대로 출력됨. 외부에서 받은 숫자를 열거형으로 바꿀 때는 Enum.IsDefined()로 확인하는 것이 안전함.

csharp
// 범위에 없는 값 출력과 Enum.IsDefinded 사용 예시
Direction dir = (Direction)99;

Console.WriteLine(dir); // 출력결과 99
Console.WriteLine($"목록에 있는 값인가: {Enum.IsDefined(typeof(Direction), dir)}");

enum Direction
{
    Up,
    Down,
    Left,
    Right
}

Enum 클래스의 도우미

Enum은 열거형을 다루는 정적 메서드를 모아 둔 곳임.

이름과 값 목록 얻기

csharp
// 이름과 값 목록 얻는 예시
string[] names = Enum.GetNames(typeof(Rarity)); // 이름 목록만 String배열로 가져와서 출력
foreach (string name in names)
{
    Console.WriteLine($"  {name}");
}

Array values = Enum.GetValues(typeof(Rarity));  // 값을 가져와서 이름=값 형태로 출력
foreach (Rarity rarity in values)
{
    Console.WriteLine
    ($"  {rarity} = {(int)rarity}");
}

enum Rarity
{
    Common = 1,
    Rare = 5,
    Legendary = 10
}

typeof(Rarity)는 그 타입 자체를 값으로 넘기는 표기임. 어떤 열거형을 다룰지 알려 주는 것임

문자열에서 열거형으로 Enum.TryParse()

사용자가 입력한 문자열을 열거형으로 바꿀 때 씀. Enum.TryParse()는 실패해도 멈추지 않고 false를 반환함. 기본적으로 대소문자를 가리며 이를 무시하려면 인수를 하나 더 넘겨야함.

csharp
if(Enum.TryParse("rare", out Rarity rarity))
{
    Console.WriteLine($"대소문자 가림: {rarity}");  // false반환으로 출력 안됨
}
if(Enum.TryParse("rare", true, out Rarity rarity))
{
    Console.WriteLine($"대소문자 무시: {rarity}");  // Rare 출력
}

진법

여기부터는 여러 상태를 한 변수에 담는 방법으로 넘어감. 그러려면 값이 비트 단위로 어떻게 저장되는지를 봐야 함.

자리값

우리가 쓰는 10진법은 자리마다 10의 거듭제곱을 곱함. 2진법은 같은 방식으로 2의 거듭제곱을 곱함. 쓸 수 있는 숫자는 0과 1뿐임.

ini
1  0  1  0   (10진법)
│  │  │  └── 0 × 1    = 0
│  │  └───── 1 × 10   = 10
│  └──────── 0 × 100  = 0
└─────────── 1 × 1000 = 1000
                      = 1010

1  0  1  0   (2진법)
│  │  │  └── 0 × 1 = 0
│  │  └───── 1 × 2 = 2
│  └──────── 0 × 4 = 0
└─────────── 1 × 8 = 8
                   = 10

컴퓨터가 2진법을 쓰는 이유는 저장 장치가 두 가지 상태만 구분하기 때문임. 이 한 자리를 비트(bit) 라고 부르고, 8비트를 묶어 바이트(byte) 라고 부름.

자리8번째7번째6번째5번째4번째3번째2번째1번째
값1286432168421

2진수 리터럴

코드에 2진수를 그대로 적을 수 있음. 0b를 앞에 붙임. 밑줄은 숫자 리터럴에서 자릿수를 구분하며 값에는 영향을 주지 않음. 숫자 사이와 진법 접두사 뒤 등 허용된 위치에 쓰고, 리터럴 끝이나 소수점 바로 옆에는 붙이지 않음. 이진수는 네 자리씩 끊으면 읽기 쉬움.

csharp
// 2진수 리터럴 사용 예시
int a = 0b1010;
int b = 0b0000_1010;      // 밑줄로 자리를 나눠 읽기 쉽게 함
int c = 0b1111_1111;

Console.WriteLine($"0b1010      = {a}");
Console.WriteLine($"0b0000_1010 = {b}");
Console.WriteLine($"0b1111_1111 = {c}");

16진수

2진수는 자리가 너무 많아 읽기 어려움. 16진수는 2진수 네 자리를 한 글자로 줄여 적는 표기임. 0x를 앞에 붙이고 10부터는 A~F를 씀.

csharp
// 16진수 리터럴 사용 예시
Console.WriteLine($"0xF    = {0xF}");
Console.WriteLine($"0xFF   = {0xFF}");
Console.WriteLine($"0x10   = {0x10}");
Console.WriteLine($"0xFFFF = {0xFFFF}");

// 같은 값을 세 가지 표기로
Console.WriteLine($"{255} = {0xFF} = {0b1111_1111}");

2진수 네 자리와 16진수 한 자리의 대응은 아래 표처럼 됨.

2진수16진수10진수2진수16진수10진수
000000100088
000111100199
0010221010A10
0011331011B11
0100441100C12
0101551101D13
0110661110E14
0111771111F15

색을 #FF8800처럼 적는 것도 16진수임. 두 자리씩 빨강·초록·파랑의 세기를 나타냄.

진법 사이의 변환

Convert클래스로 진법을 바꿀 수 있음.

csharp
// 진법간 변환 예시
int number = 42;
Console.WriteLine($"{number} → 2진수 {Convert.ToString(number, 2)}");
Console.WriteLine($"{number} → 8진수 {Convert.ToString(number, 8)}");
Console.WriteLine($"{number} → 16진수 {Convert.ToString(number, 16)}");

// 문자열에서 되돌리기
Console.WriteLine($"2진수 101010 → {Convert.ToInt32("101010", 2)}");
Console.WriteLine($"16진수 2A     → {Convert.ToInt32("2A", 16)}");

서식 지정으로 16진수를 출력할 수도 있음.

csharp
// 16진수로 서식을 지정하며 출력하는 예시
int number = 255;
Console.WriteLine($"{number:X}");      // 출력결과 대문자 FF
Console.WriteLine($"{number:x}");      // 출력결과 소문자 ff
Console.WriteLine($"{number:X4}");     // 출력결과 네 자리로 채움 00FF

비트 연산자

비트 단위로 다룬다는 것

지금까지 쓴 연산자는 값 전체를 다뤘음. 비트 연산자는 두 값을 같은 자리끼리 비교해 자리마다 결과를 냄.

연산자이름결과가 1이 되는 경우
&AND양쪽 자리가 모두 1
|OR한쪽이라도 1
^XOR두 자리가 서로 다름
~NOT원래가 0(단항 연산자)

AND - &

양쪽이 모두 1인 자리만 1이됨.

csharp
// AND 연산 예시
int x = 0b1010;      // 10
int y = 0b1100;      // 12
int result = x & y;

Console.WriteLine($"  {Convert.ToString(x, 2).PadLeft(4, '0')}  ({x})");
Console.WriteLine($"& {Convert.ToString(y, 2).PadLeft(4, '0')}  ({y})");
Console.WriteLine("  ----");
Console.WriteLine($"  {Convert.ToString(result, 2).PadLeft(4, '0')}  ({result})");

구분8의 자리4의 자리2의 자리1의 자리10진수 값
x101010
y110012
&결과10008

OR - |

한쪽이라도 1이면 1이 됨.

csharp
// OR 연산 예시
int x = 0b1010;      // 10
int y = 0b1100;      // 12
int result = x | y;

Console.WriteLine($"  {Convert.ToString(x, 2).PadLeft(4, '0')}  ({x})");
Console.WriteLine($"| {Convert.ToString(y, 2).PadLeft(4, '0')}  ({y})");
Console.WriteLine("  ----");
Console.WriteLine($"  {Convert.ToString(result, 2).PadLeft(4, '0')}  ({result})");

구분8의 자리4의 자리2의 자리1의 자리10진수 값
x101010
y110012
&결과111014

XOR - ^

두 자리가 서로 다를 때만 1이 됨.

csharp
// XOR 연산 예시
int x = 0b1010;      // 10
int y = 0b1100;      // 12
int result = x ^ y;

Console.WriteLine($"  {Convert.ToString(x, 2).PadLeft(4, '0')}  ({x})");
Console.WriteLine($"^ {Convert.ToString(y, 2).PadLeft(4, '0')}  ({y})");
Console.WriteLine("  ----");
Console.WriteLine($"  {Convert.ToString(result, 2).PadLeft(4, '0')}  ({result})");

구분8의 자리4의 자리2의 자리1의 자리10진수 값
x101010
y110012
&결과01106

XOR(^)에는 성질이 하나 있음. 같은 값으로 두번 연산하면 원래의 값으로 돌아옴.

csharp
// XOR 두번 연산 예시
int original = 0b1010;
int key = 0b0110;

int scrambled = original ^ key;
int restored = scrambled ^ key;

Console.WriteLine($"원래 값  {Convert.ToString(original, 2).PadLeft(4, '0')}");     // 1010
Console.WriteLine($"섞은 값  {Convert.ToString(scrambled, 2).PadLeft(4, '0')}");    // 1100
Console.WriteLine($"되돌림   {Convert.ToString(restored, 2).PadLeft(4, '0')}");     // 1010

이러한 성질 때문에 XOR은 켜고 끄기를 번갈아 하는 토글에 쓰임. 그리고 기초적인 파일 암호화 방법이기도 함. Key가 되는 값으로 XOR 연산해서 파일에 저장하고 불러올때 다시 Key가 되는 값으로 XOR 연산하면 원래의 값으로 돌아오기 때문임. Key가 되는 값을 모르면 원래의 값이 무엇인지 알기 어려움.

NOT - ~

피연산자가 하나뿐인 연산자임. 모든 비트를 뒤집음.(bool 형식의 ! 연산과 비슷하다.)

csharp
// NOT 연산 예시
byte x = 0b0000_1010;                       // 10
Console.WriteLine($"{x}");                  // 10 출력됨

// byte 로 되돌려 8비트만 봄
byte flipped = (byte)~x;                    // byte로 형변환
Console.WriteLine($"{flipped}");            // 245 출력됨

// int 로 계산하면 32비트가 모두 뒤집힘
Console.WriteLine($"int 로 계산하면 {~x}");  // 형변환 없이 int로 계산하면 -11 출력됨

구분128의 자리64의 자리32의 자리16의 자리8의 자리4의 자리2의 자리1의 자리10진수 값
원래 값0000101010
~ 결과11110101245

~x의 결과가 음수인 이유는 byte가 계산 중에 int로 넓혀지고, int의 맨 위 비트가 부호를 나타내기 때문임. 32비트를 전부 뒤집으면 맨 위 비트가 1이 되어 음수가 됨. 비트 하나만 보고 싶으면 그 폭의 타입으로 되돌려야 함.

시프트 연산자

비트를 좌우로 밀어내는 연산자임

연산자이름동작
<<왼쪽 시프트비트를 왼쪽으로 밀고 오른쪽을 0으로 채움
>>오른쪽 시프트비트를 오른쪽으로 밀고 왼쪽을 부호로 채움

왼쪽 시프트 - <<

csharp
// 왼쪽 시프트 사용 예시
int number = 0b0000_0011;      // 3

for (int shift = 0; shift <= 4; shift++)
{
    int shifted = number << shift;
    Console.WriteLine($"3 << {shift} = {shifted,3}  {Convert.ToString(shifted, 2).PadLeft(8, '0')}");
}

ini
실행결과
3 << 0 =   3  00000011
3 << 1 =   6  00000110
3 << 2 =  12  00001100
3 << 3 =  24  00011000
3 << 4 =  48  00110000

한 칸 밀 때마다 값이 두 배가 됨. 자리값이 두 배씩 커지기 때문임. x << n은 x에 2의 n승을 곱한 것과 같음.

오른쪽 시프트 - >>

csharp
using System;

int number = 48;

for (int shift = 0; shift <= 4; shift++)
{
    int shifted = number >> shift;
    Console.WriteLine($"48 >> {shift} = {shifted,3}  {Convert.ToString(shifted, 2).PadLeft(8, '0')}");
}

ini
실행결과
48 >> 0 =  48  00110000
48 >> 1 =  24  00011000
48 >> 2 =  12  00001100
48 >> 3 =   6  00000110
48 >> 4 =   3  00000011

한 칸 밀 때마다 절반이 됨. 밀려 나간 비트는 사라지므로 나머지가 버려지는 정수 나눗셈과 같음.

음수를 오른쪽으로 밀 때

왼쪽에 생기는 빈 자리는 부호 비트로 채워짐. 양수면 0, 음수면 1이 들어가 부호가 유지됨.

csharp
// 음수에 대한 오른쪽 시프트 일때의 예시
Console.WriteLine($" 24 >> 2 = {24 >> 2}");     // 출력결과 6
Console.WriteLine($"-24 >> 2 = {-24 >> 2}");    // 출력결과 -6
Console.WriteLine($" -1 >> 5 = {-1 >> 5}");     // 출력결과 -1

  • -1은 모든 비트가 1인 값이므로, 아무리 밀어도 왼쪽에서 1이 계속 채워져 1로 남음.

2의 거듭제곱 만들기

1 << n은 n번째 비트만 1인 값이 됨. 플래그를 정의할 때 이 표기를 씀.

csharp
// 시프트로 거듭제곱 만드는 예시
for (int n = 0; n < 6; n++)
{
    int value = 1 << n;
    Console.WriteLine($"1 << {n} = {value,2}  {Convert.ToString(value, 2).PadLeft(6, '0')}");
}

ini
실행결과
1 << 0 =  1  000001
1 << 1 =  2  000010
1 << 2 =  4  000100
1 << 3 =  8  001000
1 << 4 = 16  010000
1 << 5 = 32  100000

비트가 한 자리씩 왼쪽으로 옮겨 가는 것이 보임. 자리마다 하나씩 다른 것을 뜻하게 하려면 값이 이렇게 겹치지 않아야 함.

비트 플래그

여러 상태를 한 변수에 담기

캐릭터가 여러 버프를 동시에 받고 있다고 하자. 열거형 변수 하나에는 값이 하나만 들어가므로 이대로는 표현할 수 없음. 비트를 쓰면 한 변수로 해결됨. 자리마다 하나씩 다른 것을 뜻하게 하고, 1이면 켜진 것으로 봄.

plain text
상태저장 예시
0  0  1  0  1
│  │  │  │  └── 공격력 증가  켜짐
│  │  │  └───── 방어력 증가  꺼짐
│  │  └──────── 이동 속도    켜짐
│  └─────────── 체력 회복    꺼짐
└────────────── 마나 회복    꺼짐

정수 하나로 상태 다섯 개를 담았음. int는 32비트이므로 이런 상태를 최대 32가지까지 담을 수 있음

[Flags] 어트리뷰트

플래그로 쓸 열거형은 값을 2의 거듭제곱으로 정하고, 선언 앞에 [Flags]를 붙임.

csharp
// [Flags] 사용한 열거형 예시
Buff buff = Buff.AttackUp | Buff.SpeedUp;
Console.WriteLine(buff);    // 출력 내용 AttackUp, SpeedUp

[Flags]
enum Buff
{
    None = 0,
    AttackUp = 1,          // 0000 0001
    DefenseUp = 1 << 1,    // 0000 0010
    SpeedUp = 1 << 2,      // 0000 0100
    Regen = 1 << 3         // 0000 1000
}

[Flags]는 비트 조합으로 사용할 열거형임을 표시함. ToString()은 선언된 이름들로 표현할 수 있는 조합이면 이름을 이어 출력하고, 표현할 수 없는 값이면 숫자를 출력할 수 있음. 어트리뷰트를 붙이지 않아도 단일 선언값은 이름으로 출력됨.

csharp
// [Flags] 사용한 상태와 아닌상태 출력 비교 예시
WithFlags a = WithFlags.A | WithFlags.B;
WithoutFlags b = WithoutFlags.A | WithoutFlags.B;

Console.WriteLine($"[Flags] 붙임   : {a}");     //출력 내용 [Flags] 붙임   : A, B
Console.WriteLine($"[Flags] 안 붙임 : {b}");    //출력 내용 [Flags] 안 붙임 : 3

[Flags]
enum WithFlags
{
    A = 1,
    B = 2
}

enum WithoutFlags
{
    A = 1,
    B = 2
}

  • 참고: 대괄호로 감싼 [Flags] 같은 표기를 어트리뷰트(attribute) 라고 함. 타입이나 멤버에 추가 정보를 붙여 두면 .NET이 그것을 보고 동작을 바꿈. 여기서는 “이 열거형은 여러 값을 조합해 쓴다”고 알리는 용도로만 씀.
  • 참고2: [Flags] 열거형 사용할 때 자동할당하면 의도한 대로 잘 안되어서 일일이 할당해주어야함. 문자열 변환(ToString())도 편리함.

[Flags]가 없어도 비트 연산 자체는 동작함. 하지만 출력이 숫자로 나와 디버깅이 어려워지므로 플래그로 쓸 열거형에는 반드시 붙임.

값이 겹치면 안 됨

플래그의 값은 서로 비트가 겹치지 않아야 함. 기본값 0, 1, 2, 3…을 그대로 쓰면 3번째 항목부터 어긋남.

csharp
// 값이 겹치게 선언한 예시
Wrong w = Wrong.A | Wrong.B;
Console.WriteLine($"A({(int)Wrong.A}) | B({(int)Wrong.B}) = {(int)w} → {w}");
Console.WriteLine($"C 가 켜져 있는가: {(w & Wrong.C) != 0}");

[Flags]
enum Wrong
{
    None = 0,
    A = 1,
    B = 2,
    C = 3        // 잘못됨 — 0011 은 A 와 B 를 합친 것과 같음
}

A와 B만 켰는데 C도 켜져 있다고 나옴. 1 | 2가 3이고 그것이 C의 값이기 때문임. 그래서 1 << n 표기를 쓰는 것임 —> 값이 겹칠 수 없게 됨.

켜기, 확인, 끄기, 뒤집기

플래그를 다루는 네가지 동작이 각각 연산자 하나에 대응함.

동작표기원리
켜기flags |= XOR - 그 자리를 1로 만듦
확인(flags & X) != 0AND - 그 자리만 남겨 0인지 확인
끄기flags &= ~XAND NOT - 그 자리만 0으로 만듦
뒤집기flags ^= XXOR - 켜져 있으면 끄고 꺼져 있으면 켬
csharp
// 켜기, 확인, 끄기, 뒤집기 사용 예시
Buff buff = Buff.None;
Show("초기", buff);

// 켜기
buff |= Buff.AttackUp;
Show("공격력 증가 켬", buff);

buff |= Buff.SpeedUp;
Show("이동 속도 켬", buff);

// 확인
Console.WriteLine($"공격력 증가? {(buff & Buff.AttackUp) != 0}");
Console.WriteLine($"방어력 증가? {(buff & Buff.DefenseUp) != 0}");

// 끄기
buff &= ~Buff.AttackUp;
Show("공격력 증가 끔", buff);

// 뒤집기
buff ^= Buff.Regen;
Show("체력 회복 뒤집음", buff);

buff ^= Buff.Regen;
Show("체력 회복 다시 뒤집음", buff);

void Show(string label, Buff value)
{
    string bits = Convert.ToString((int)value, 2).PadLeft(4, '0');
    Console.WriteLine($"{bits}  {value,-20}  {label}");
}

[Flags]
enum Buff
{
    None = 0,
    AttackUp = 1,
    DefenseUp = 1 << 1,
    SpeedUp = 1 << 2,
    Regen = 1 << 3
}

yaml
실행결과
0000  None                  초기
0001  AttackUp              공격력 증가 켬
0101  AttackUp, SpeedUp     이동 속도 켬
공격력 증가? True
방어력 증가? False
0100  SpeedUp               공격력 증가 끔
1100  SpeedUp, Regen        체력 회복 뒤집음
0100  SpeedUp               체력 회복 다시 뒤집음

끄기가 &= ~X인 이유를 비트로 보면 이렇게 됨.

yaml
        0101   현재 (AttackUp | SpeedUp)
~0001 = 1110   끄려는 자리만 0, 나머지는 1
------------   AND
        0100   그 자리만 꺼지고 나머지는 그대로

HasFlag

HasFlag(X)는 X의 모든 비트가 포함되는지 확인하며 (flags & X) == X와 같은 판정임. X가 0이면 true임. 0이 아닌 단일 비트일 때는 (flags & X) != 0과도 결과가 같음. 단 HasFlag()에는 함정이 하나 있음. None을 넘기면 언제나 True가 나옴. None은 값이 0이고 x & 0은 언제나 0이므로 “포함한다”는 판정이 늘 성립함. 아무 플래그도 없는 상태는 == None``으로 확인해야 함.

여럿을 묶은 항목

자주 쓰는 조합에 이름을 붙여 둘 수 있음.

csharp
// Flags에서 항목 조합하는 예시
[Flags]
enum Buff
{
    None = 0,
    AttackUp = 1,
    DefenseUp = 1 << 1,
    SpeedUp = 1 << 2,
    Regen = 1 << 3,

    // 조합에 이름 붙이기
    Offensive = AttackUp | SpeedUp,
    Defensive = DefenseUp | Regen,
    All = Offensive | Defensive
}

조합 항목은 새 비트를 쓰지 않고 기존 비트를 합친 것이므로 겹침 규칙을 어기지 않음.

켜진 것만 훑기

Enum.GetValues()로 항목을 돌며 켜진 것을 찾을 수 있음.

csharp
// 켜진 것만 찾는 예시
Buff buff = Buff.AttackUp | Buff.Regen;

Console.WriteLine($"현재 버프: {buff}");
Console.WriteLine("켜진 항목");

foreach (Buff flag in Enum.GetValues(typeof(Buff)))
{
    // None 과 조합 항목은 건너뜀 — 비트가 하나뿐인 것만 봄
    if (flag == Buff.None || ((int)flag & ((int)flag - 1)) != 0)
    {
        continue;
    }

    if ((buff & flag) != 0)
    {
        Console.WriteLine($"  {flag}");
    }
}

[Flags]
enum Buff
{
    None = 0,
    AttackUp = 1,
    DefenseUp = 1 << 1,
    SpeedUp = 1 << 2,
    Regen = 1 << 3,
    Offensive = AttackUp | SpeedUp
}

plain text
실행결과
현재 버프: AttackUp, Regen
켜진 항목
  AttackUp
  Regen

(x & (x - 1)) != 0은 비트가 둘 이상 켜져 있는지 보는 표기임. 2의 거듭제곱이면 x - 1이 아래 비트를 모두 채우므로 AND 결과가 0이 됨.

csharp
// (x & (x - 1)) == 0 예시
int[] values = { 1, 2, 3, 4, 5, 8, 12 };

foreach (int value in values)
{
    bool single = (value & (value - 1)) == 0;
    Console.WriteLine($"{value,2} = {Convert.ToString(value, 2).PadLeft(4, '0')}  비트 하나뿐? {single}");
}

ini
실행결과
 1 = 0001  비트 하나뿐? True
 2 = 0010  비트 하나뿐? True
 3 = 0011  비트 하나뿐? False
 4 = 0100  비트 하나뿐? True
 5 = 0101  비트 하나뿐? False
 8 = 1000  비트 하나뿐? True
12 = 1100  비트 하나뿐? False