확장 메서드는 붙는 것이 아니라 붙어 보이는 것이다
Esc

    확장 메서드는 붙는 것이 아니라 붙어 보이는 것이다

    –C#

    문자열이 이메일처럼 생겼는지 확인하는 코드를 자주 쓴다고 해 봅시다.

    if (검사(입력)) { ... }

    이렇게 쓰는 것보다 이렇게 쓰고 싶습니다.

    if (입력.이메일인가()) { ... }

    그런데 string 은 우리가 만든 클래스가 아닙니다. 열어서 메서드를 하나 더 넣을 수가 없습니다. 상속을 하려 해도 string 은 sealed 라 막혀 있습니다.

    고칠 수 없는 클래스에도 메서드를 붙여 쓸 수 있습니다. 진짜로 붙는 것은 아니고, 붙은 것처럼 부를 수 있게 해 줍니다.

    확장 메서드는 이렇게 생겼다

    조건이 세 개입니다. 하나라도 빠지면 안 됩니다.

    static class 문자열도구{ public static bool 이메일인가(this string s) { ... }}1. 담는 클래스가 static2. 메서드도 static3. 첫 매개변수 앞에 thisthis 가 붙은 그 타입에 메서드가 생긴 것처럼 보입니다.
    세 조건 중 하나라도 빠지면 그냥 평범한 static 메서드입니다.
    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 같은 것이 전부 이 장치 위에 있습니다