확장 메서드는 붙는 것이 아니라 붙어 보이는 것이다
문자열이 이메일처럼 생겼는지 확인하는 코드를 자주 쓴다고 해 봅시다.
if (검사(입력)) { ... }
이렇게 쓰는 것보다 이렇게 쓰고 싶습니다.
if (입력.이메일인가()) { ... }
그런데 string 은 우리가 만든 클래스가 아닙니다. 열어서 메서드를 하나 더 넣을 수가 없습니다.
상속을 하려 해도 string 은 sealed 라 막혀 있습니다.
고칠 수 없는 클래스에도 메서드를 붙여 쓸 수 있습니다. 진짜로 붙는 것은 아니고, 붙은 것처럼 부를 수 있게 해 줍니다.
확장 메서드는 이렇게 생겼다
조건이 세 개입니다. 하나라도 빠지면 안 됩니다.
static class 문자열도구
{
public static bool 이메일인가(this string s)
{
return s.Contains("@") && s.Contains(".");
}
}
이제 이렇게 부를 수 있습니다.
string 입력 = "a@b.com";
Console.WriteLine(입력.이메일인가()); // True
string 은 한 글자도 안 고쳤는데 .이메일인가() 가 생겼습니다.
진짜로 붙은 것이 아니다
여기가 이 글의 전부입니다. 컴파일러는 입력.이메일인가() 를 보면 그냥 static 메서드 호출로 바꿔 씁니다.
string 안에는 아무것도 안 들어갔습니다. 부르는 모양만 바뀐 것입니다.
그래서 확장 메서드는 문법 설탕이라고 부릅니다.
이걸 알면 나머지 규칙이 전부 저절로 나옵니다. 외울 것이 없습니다.
여기서 나오는 규칙들
private 은 못 봅니다. 밖에서 부르는 static 메서드니까요.
public static void 출력(this 캐릭터 c)
{
Console.WriteLine(c.체력); // public 이면 됩니다
Console.WriteLine(c._내부값); // private 이면 에러
}
진짜 멤버였다면 private 도 봤을 겁니다. 확장 메서드는 남입니다.
이름이 겹치면 원래 것이 이깁니다.
static class 도구
{
public static string ToString(this int n) => "내 것";
}
Console.WriteLine(5.ToString()); // "5". 내 것이 아닙니다
컴파일러는 진짜 멤버를 먼저 찾고, 없을 때만 확장 메서드를 찾습니다. 그래서 확장 메서드로 기존 동작을 덮어쓸 수 없습니다. 이건 다행입니다. 남의 코드가 갑자기 다르게 동작하면 아무도 못 찾습니다.
using 이 필요합니다. 확장 메서드는 그 클래스가 있는 네임스페이스를 using 해야 보입니다.
using 내도구; // 이게 없으면 .이메일인가() 가 안 보입니다
메서드가 분명히 있는데 자동완성에 안 나오면 대개 이것입니다.
LINQ 를 쓰려고 using System.Linq; 를 적는 이유도 같습니다.
null 이어도 안 터집니다. 이건 좀 신기한 자리입니다.
string s = null;
Console.WriteLine(s.이메일인가());
진짜 메서드였다면 이 순간 NullReferenceException 이 납니다. 객체가 없는데 그 객체의 메서드를 부를 수 없으니까요.
그런데 확장 메서드는 문자열도구.이메일인가(null) 로 바뀝니다. 매개변수로 null 이 들어가는 것뿐입니다.
호출은 멀쩡히 되고, 안에서 s.Contains 를 하는 순간 터집니다.
그래서 확장 메서드는 안에서 null 검사를 직접 해야 합니다.
public static bool 이메일인가(this string s)
{
if (s == null) return false;
return s.Contains("@") && s.Contains(".");
}
우리가 이미 쓰고 있었다
List 나 배열에 이런 것을 써 본 적이 있다면 그게 전부 확장 메서드입니다.
using System.Linq;
int[] 점수 = { 30, 90, 70 };
int 합 = 점수.Sum();
int 최고 = 점수.Max();
int[] 에는 Sum 이라는 메서드가 없습니다. 배열은 우리가 만든 것도 아니고 고칠 수도 없습니다.
System.Linq.Enumerable 이라는 static 클래스가 확장 메서드로 붙여 준 것입니다.
메서드 하나를 붙이는 이 작은 장치 위에 라이브러리 하나가 통째로 올라가 있습니다. 그 이야기는 뒤에서 따로 합니다.
언제 쓰고 언제 참나
쓸 만한 자리. 고칠 수 없는 타입(string, int, 남의 라이브러리, Unity 의 Transform)에
그 타입만 있으면 답이 나오는 기능을 붙일 때입니다.
public static Vector3 XZ만(this Vector3 v)
{
return new Vector3(v.x, 0f, v.z);
}
참아야 할 자리. 내가 만든 클래스입니다.
내 클래스면 그냥 안에 메서드를 넣으면 됩니다. 굳이 밖으로 뺄 이유가 없고,
밖으로 빼면 private 을 못 보니 오히려 불편해집니다.
그리고 아무 타입에나 붙이지 않는 편이 좋습니다.
public static void 출력(this object o) { ... } // 모든 것에 붙습니다
이러면 세상의 모든 변수 뒤에 .출력() 이 뜹니다. 자동완성이 쓰레기가 됩니다.
정리
- 담는 클래스가
static, 메서드도static, 첫 매개변수 앞에this. 세 개가 다 있어야 합니다 - 진짜로 붙는 것이 아니라 컴파일러가 static 호출로 바꿔 쓰는 것입니다
- 그래서
private을 못 보고, 이름이 겹치면 원래 멤버가 이기고,using이 필요합니다 null로 불러도 호출 자체는 됩니다. 안에서 검사해야 합니다Sum,Max,Where같은 것이 전부 이 장치 위에 있습니다