같은지 보는 것과 줄 세우는 것은 다른 일이다
앞 글에서 Replace 가 대소문자를 구분한다고 했습니다. 그러면 이건 어떨까요.
Console.WriteLine("Hello" == "hello");
답은 False 입니다. 그런데 여기서 물어볼 게 하나 더 있습니다.
애초에 == 로 문자열을 비교하는 게 되는 게 맞나요.
== 는 내용을 비교한다
됩니다. 그리고 우리가 기대하는 대로 동작합니다.
string a = "Hello";
string b = "Hel" + "lo";
Console.WriteLine(a == b); // True
a 와 b 는 각각 다른 자리에 만들어진 문자열인데도 True 입니다.
두 문자열이 담고 있는 글자를 처음부터 끝까지 비교하기 때문입니다.
당연해 보이지만 당연한 일은 아닙니다. C# 에서 == 는 보통 담긴 값이 같은지를 봅니다.
문자열처럼 크기가 정해지지 않은 것은 변수에 내용 자체가 아니라 내용이 있는 자리가
담기는데, 그러면 == 는 자리가 같은지를 보게 됩니다. 자리가 다르면 내용이 같아도 False 입니다.
C# 은 문자열에 한해 == 를 내용 비교로 따로 정해 두었습니다. 문자열을 비교할 일이
너무 흔해서, 매번 메서드를 부르게 하는 대신 기호를 특별 대우한 것입니다.
변수에 값이 담기는 경우와 자리가 담기는 경우의 차이는 클래스를 다룰 때 제대로 봅니다. 지금은 문자열의
==는 내용을 본다만 기억하면 됩니다. 다른 언어를 하다 온 경우 이 부분이 다를 수 있어서, 문자열 비교에==를 못 쓰는 언어도 있습니다.
Equals 도 결과가 같습니다.
a.Equals(b); // True
a == b; // True
한쪽이 null 일 수 있으면 == 가 안전합니다. a 가 null 인데 a.Equals(b) 를 부르면
없는 것에게 말을 거는 것이라 예외가 납니다.
대소문자를 무시하려면
여기서 ToLower() 로 양쪽을 맞춰 비교하고 싶어집니다. 흔한 방법이고 대체로 동작합니다.
그런데 권하지 않습니다.
이유가 셋입니다.
첫째, 문자열이 두 개 새로 만들어집니다. 앞에서 본 그대로입니다. ToLower() 는
원본을 고치지 않고 새 문자열을 돌려주므로, 비교 한 번에 쓰고 버릴 문자열이 둘 생깁니다.
반복문 안이면 그만큼 쌓입니다.
둘째, 결과가 컴퓨터의 언어 설정에 따라 달라질 수 있습니다. 가장 유명한 예가 터키어입니다.
터키어에는 점 있는 i 와 점 없는 ı 가 서로 다른 글자로 있습니다.
그래서 터키어 설정에서는 "I".ToLower() 가 우리가 아는 "i" 가 아니라 점 없는 "ı" 입니다.
// 터키어 설정의 컴퓨터에서
"IMAGE".ToLower() == "image" // False
한국에서 만든 코드가 터키에서만 조용히 다르게 도는 셈입니다. 찾기 어려운 종류의 버그입니다.
셋째, 의도가 코드에 안 적혀 있습니다. ToLower() 두 번은 “대소문자를 무시하고 싶다” 가
아니라 “소문자로 바꾸고 싶다” 라고 적혀 있습니다. OrdinalIgnoreCase 는 이름이 곧 의도입니다.
서수 비교와 문화권 비교
StringComparison 에는 크게 두 갈래가 있습니다. 이름이 길어 보이지만 나누는 기준은 하나입니다.
글자에 붙은 번호를 볼 것인가, 사람이 쓰는 언어의 규칙을 볼 것인가.
| 값 | 무엇을 보나 |
|---|---|
Ordinal |
앞에서 본 글자 번호를 그대로 비교 |
OrdinalIgnoreCase |
번호 비교, 단 대소문자는 같게 취급 |
CurrentCulture |
실행하는 컴퓨터의 언어 규칙 |
CurrentCultureIgnoreCase |
언어 규칙, 대소문자 무시 |
InvariantCulture |
어디서 돌려도 같은 언어 규칙 |
차이가 드러나는 자리는 정렬입니다.
"Zebra" 와 "apple" 중 어느 쪽이 앞인가
Ordinal → "Zebra" 'Z' 는 90, 'a' 는 97
CurrentCulture → "apple" 사전에서는 a 가 z 앞에 온다
앞에서 대문자 번호가 소문자 번호보다 작다고 했습니다. 그래서 번호로만 세우면 대문자로 시작하는 것이 전부 앞으로 몰립니다. 사람이 기대하는 사전 순서가 아닙니다.
고르는 기준은 단순합니다.
사람에게 보여줄 것이면 문화권 비교, 프로그램이 쓸 것이면 서수 비교.
화면에 뿌릴 이름 목록을 정렬한다면 문화권 비교가 맞습니다. 반대로 아이디, 파일 경로, 설정 키, 통신에 쓰는 값처럼 사람이 읽으라고 만든 게 아닌 문자열은 서수 비교가 맞습니다. 빠르고, 어느 나라에서 돌려도 결과가 같습니다.
비교 방식을 안 넘기면 메서드마다 기본값이 다릅니다. == 와 Equals 는 서수 비교이고,
string.Compare 는 문화권 비교입니다. 헷갈리기 쉬운 자리라, 대소문자나 정렬이 걸린
비교라면 방식을 직접 적어 주는 편이 낫습니다.
순서를 알려면 Compare
== 는 같은지 아닌지만 알려줍니다. 정렬하려면 어느 쪽이 앞인지를 알아야 합니다.
string.Compare("apple", "banana"); // 음수 apple 이 앞
string.Compare("banana", "apple"); // 양수 banana 가 뒤
string.Compare("apple", "apple"); // 0 같다
"apple".CompareTo("banana"); // 음수 같은 일을 하는 메서드
중요한 것은 부호뿐입니다. 음수면 앞, 0이면 같음, 양수면 뒤입니다.
-1 이 나오는 경우가 많지만 그 숫자에 의미가 있는 건 아니라서,
Compare(a, b) == -1 처럼 값을 직접 비교하면 안 됩니다.
if (string.Compare(a, b) < 0) // 이렇게
if (string.Compare(a, b) == -1) // 이렇게 말고
서수 비교라면 이 부호가 어디서 나오는지도 뻔합니다. 처음으로 다른 글자를 만난 자리에서 두 글자의 번호를 뺀 것입니다. 앞에서 본 번호가 그대로 쓰입니다.
비어 있는 문자열
비교에서 자주 걸리는 마지막 자리입니다. null 과 "" 는 다릅니다.
string a = null; // 문자열이 아예 없다
string b = ""; // 문자열은 있는데 글자가 0개다
a.Length 는 예외가 나고 b.Length 는 0 입니다. 둘을 한 번에 확인하는 메서드가 있습니다.
string.IsNullOrEmpty(input); // null 이거나 "" 이면 true
string.IsNullOrWhiteSpace(input); // 위에 더해 공백뿐이어도 true
사용자 입력에는 두 번째가 맞습니다. 스페이스만 치고 엔터를 누른 입력은
"" 가 아니라 " " 라서 첫 번째로는 안 걸립니다.
앞 글에서 입력을 받자마자 Trim 하라고 한 것과 같은 이야기입니다.
정리
- 문자열의
==는 내용을 비교한다. C# 이 문자열에만 특별히 그렇게 정해 두었다. Equals도 같지만, 한쪽이null일 수 있으면==가 안전하다.- 대소문자를 무시할 때
ToLower()로 맞추지 않는다. 문자열이 두 개 생기고, 언어 설정을 탄다. Equals(b, StringComparison.OrdinalIgnoreCase)처럼 비교 방식을 넘긴다.- 서수 비교는 글자 번호를, 문화권 비교는 언어의 정렬 규칙을 본다.
- 사람에게 보여줄 것이면 문화권 비교, 프로그램이 쓸 것이면 서수 비교.
Compare와CompareTo는 부호만 의미가 있다.== -1로 비교하지 않는다.null과""는 다르다. 사용자 입력은IsNullOrWhiteSpace로 확인한다.