(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 이면 오른쪽은 실행되지 않는다
}
순서를 뒤집으면 player가 null일 때 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은 예외다 - 실수는
==대신 오차 범위로 비교한다