포스트

(C#) 2. 데이터 연산

비교 연산과 논리 연산으로 조건을 만드는 방법. &&와 ||의 단축 평가가 왜 중요한지, &와의 차이, 그리고 실수 비교에 ==를 쓰면 안 되는 이유를 정리했다.

(C#) 2. 데이터 연산

조건을 값으로 만들기

1편에서 데이터를 담았으니, 이제 그 데이터로 판단을 해야 한다. “살아 있는가”, “고레벨인가” 같은 것이다.

여기서 처음에 어색했던 건, 비교의 결과 자체가 값이라는 점이었다. if 안에서만 쓰는 게 아니라 변수에 담아둘 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;

namespace DataOperation
{
    class Program
    {
        static void Main(string[] args)
        {
            int hp = 100;
            int level = 50;

            // < <= > >= == !=
            bool isAlive = (hp > 0);
            bool isHighLevel = (level >= 40);

            // && AND   || OR   ! NOT
            // a = 살아있는 고랩 유저인가?
            bool a = isAlive && isHighLevel;

            // b = 살아있거나, 고렙 유저이거나, 둘 중 하나인가요?
            bool b = isAlive || isHighLevel;

            // c = 죽은 유저인가?
            bool c = !isAlive;
        }
    }
}

조건에 이름을 붙여두면 if (hp > 0 && level >= 40)보다 읽기가 훨씬 낫다. isAlive, isHighLevel처럼 is 접두사를 붙이는 게 관례다.

&& 와 || 는 왼쪽에서 끝나면 오른쪽을 안 본다

이걸 알고 나서 코드 쓰는 방식이 바뀌었다.

a && b에서 a가 거짓이면 결과는 무조건 거짓이다. 그래서 C#은 b를 아예 평가하지 않는다. a || b에서 a가 참일 때도 마찬가지다. 이걸 단축 평가라고 한다.

단순히 성능 얘기가 아니라 코드의 안전성이 달라진다.

1
2
3
4
if (player != null && player.Hp > 0)
{
    // player 가 null 이면 오른쪽은 실행되지 않는다
}

순서를 뒤집으면 playernull일 때 NullReferenceException이 난다. 왼쪽 검사가 오른쪽을 지켜주는 구조다.

배열도 같다.

1
if (i < arr.Length && arr[i] > 0)   // 범위 검사가 먼저

부작용이 있는 함수를 오른쪽에 두면 반대로 함정이 된다.

1
if (isAlive && UseItem())    // isAlive 가 거짓이면 아이템을 안 쓴다

이게 의도라면 좋은데, “둘 다 실행하고 결과를 합치려는” 의도였다면 버그다. 조건 안에서 상태를 바꾸는 함수를 부르지 않는 게 안전하다.

& 와 && 는 다르다

bool끼리도 &|를 쓸 수 있다. 다른 점은 단축 평가를 하지 않는다는 것이다.

1
if (player != null & player.Hp > 0)   // 오른쪽도 반드시 평가된다 -> 예외

& 하나만 써도 컴파일이 되기 때문에 오타로 나기 쉽고, 그 순간 위의 안전장치가 사라진다.

정수에 쓰면 완전히 다른 의미가 된다. 그때는 비트 단위 AND다. 그 얘기는 3편에 정리했다.

우선순위와 괄호

&&||보다 우선순위가 높다.

1
bool r = a || b && c;      // a || (b && c) 로 해석된다

수학에서 곱셈이 덧셈보다 먼저인 것과 같은 구조인데, 코드에서는 헷갈릴 여지가 크다. 세 개 이상 섞이면 괄호를 치는 편이 낫다. 컴파일 결과는 같고 읽는 사람만 편해진다.

비교 연산자가 논리 연산자보다 먼저라서 hp > 0 && level >= 40은 괄호 없이도 의도대로 동작한다. 그래도 원 코드처럼 (hp > 0)으로 감싸두면 눈에 잘 들어온다.

if 조건에 정수를 못 넣는다

C나 C++를 먼저 봤다면 걸리는 부분이다.

1
2
int hp = 100;
if (hp)          // 컴파일 에러: int 를 bool 로 변환할 수 없다

C에서는 0이 아니면 참으로 쳤는데 C#은 bool만 받는다. 처음엔 불편했는데, 덕분에 유명한 실수 하나가 원천적으로 막힌다.

1
if (hp = 0)      // 컴파일 에러. C 였다면 대입 후 거짓이 되어 조용히 통과한다

===로 잘못 쓰는 실수가 컴파일 단계에서 잡힌다. bool 변수에 대해서는 if (isAlive = false)가 여전히 통과하지만, 그 경우는 훨씬 드물다.

== 가 항상 값 비교는 아니다

숫자는 값을 비교한다. 참조 타입은 기본적으로 같은 객체인지를 비교한다.

1
2
3
int[] a1 = { 1, 2, 3 };
int[] a2 = { 1, 2, 3 };
Console.WriteLine(a1 == a2);   // False. 내용은 같지만 다른 객체다

string은 예외다. ==가 오버로드되어 내용을 비교한다.

1
2
3
string s1 = "hello";
string s2 = "hel" + "lo";
Console.WriteLine(s1 == s2);   // True

이 예외 때문에 “C#의 ==는 내용 비교”라고 잘못 기억하기 쉽다. 문자열은 특별한 경우다. 자세한 건 24편에서 다룬다.

실수는 == 로 비교하면 안 된다

1편에서 본 것의 연장이다.

1
Console.WriteLine(0.1 + 0.2 == 0.3);   // False

2진 부동소수점이 0.1을 정확히 표현하지 못해서 미세한 오차가 남는다. 오차 범위 안이면 같다고 보는 방식으로 비교한다.

1
if (Math.Abs(x - y) < 1e-9) { }

허용 오차를 얼마로 잡을지는 값의 크기에 달렸다. 값이 크면 절대 오차도 커지므로 상대 오차로 보는 게 맞을 때도 있다.

정리하면

  • 비교 결과는 bool 값이라 변수에 담을 수 있다. 조건에 이름을 붙이면 읽기 쉬워진다
  • &&||는 단축 평가를 한다. null 검사와 범위 검사를 왼쪽에 두면 오른쪽이 보호된다
  • &, |는 단축 평가를 안 한다. 오타로 쓰면 안전장치가 사라진다
  • &&||보다 우선순위가 높다. 셋 이상 섞이면 괄호를 친다
  • C#은 if 조건에 bool만 받는다. if (hp = 0) 같은 실수가 컴파일 단계에서 잡힌다
  • 참조 타입의 ==는 같은 객체인지를 본다. string은 예외다
  • 실수는 == 대신 오차 범위로 비교한다
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.