언어/C#

C# 인터페이스

tsyang 2021. 1. 27. 22:44

선요약


  • 인터페이스는 제너릭 버전 써라.
  • 명시적 인터페이스 구현은 웬만하면 쓰지 마라. (써야할 때도 있지만...)
  • 박싱과 타입 안정성에 주의하자. (인터페이스는 참조형이다.)
  • IS-A, CAN-DO 관계로 타입과 인터페이스를 골라라.. 애매하다면 타입이 확장성과 수정이 더 용이하다는 점을 염두에 둔다.

인터페이스 상속하기


C# 컴파일러는 인터페이스 메서드를 구현할 때 public으로 선언하도록 요구한다. CLR은 인터페이스가 virtual로 정의될 것을 요구한다. 만약 소스 코드에서 메서드를 구현할 때 명시적으로 virtual 키워드를 지정하지 않으면 컴파일러가 해당 메서드에 virtual 키워드와 sealed 키워드를 포함시킨다. 이 경우 자식 클래스에서 인터페이스 메서드를 재정의할 수 없다. 만약 명시적으로 vitrual 키워드를 사용한 경우에는 재정의 할 수 있다.

 

public class Base : IDisposable
{
    //public virtual sealed void Dispose() 와 같은 의미이지만 왼쪽처럼은 쓸 수 없음
    public void Dispose() 
    {
        Console.WriteLine("Base's Dispose()");
    }
}

public class Derived : Base
{
    public override void Dispose() // 에러, base의 Dispose가 virtual인 경우는 괜찮음
    {
        Console.WriteLine("Derived's Dispose()");
    }
}

 


 

인터페이스 메서드 구현에 sealed가 포함된 경우, 자식 클래스에서 메서드를 재정의할 수 없지만 자식 클래스는 또 다시 동일 인터페이스를 상속받아 구현함으로써 자체적으로 새로운 메서드를 만들 수 있다.

 

public class Base : IDisposable
{
    public void Dispose() 
    {
        Console.WriteLine("Base's Dispose()");
    }
}

public class Derived : Base, IDisposable
{
    new public void Dispose() // 재정의를 위해 new 키워드를 사용해야 함
    {
        Console.WriteLine("Derived's Dispose()");
    }
}

public static class Program
{
    public static void Main()
    {
        Base a = new Base();
        a.Dispose();
        ((IDisposable)a).Dispose();
        /*출력 :
            Base's Dispose()
            Base's Dispose()
        */

        Base b = new Derived();
        b.Dispose();
        ((IDisposable)b).Dispose();
        /*출력 :
            Base's Dispose()
            Derived's Dispose()
        */

        Derived c = new Derived();
        c.Dispose();
        ((IDisposable)c).Dispose();

        /*출력 :
            Derived's Dispose()
            Derived's Dispose()
         */
    }
}

IDisaposable 타입으로 캐스팅 한 경우의 출력을 보면 된다.

 

 


인터페이스의 구현 (암묵적, 명시적)


 

public class Base : IDisposable, IComparable
{
    public void Dispose()  // 암묵적 구현
    {
    }

    int IComparable.CompareTo(object obj) // 명시적 구현
    {
        return 0;
    }
}

인터페이스는 암묵적 혹은 명시적으로 구현할 수 있다.

 

컴파일러는 위의 Base클래스에서 Dispose() 메서드는 IDisaposable.Dispose()를 구현한 것으로 가정한다. (암묵적 구현) 그 이유는 이 메서드가 public으로 선언되어 있으며 인터페이스 내의 메서드와 새로 정의한 메서드의 원형이 완벽히 일치하기 때문이다. 만약 Base.Dispose()가 private라면 다음과 같은 에러가 발생한다.

 

 

public class Base : IDisposable
{
    public void Dispose()  
    {
        Console.WriteLine("Base's Dispose()");
    }

    void IDisposable.Dispose() //명시적 구현
    {
        Console.WriteLine("IDisaposable's Dispose()");
    }
}

public static class Program
{
    public static void Main()
    {
        Base a = new Base();

        a.Dispose();
        ((IDisposable)a).Dispose();

        //출력
        //Base's Dispose()
        //IDisaposable's Dispose()
    }
}

 

또한 만약 위처럼 명시적으로 구현된 메서드가 있다면, 이름과 원형이 일치하더라도 다른 인터페이스의 메서드를 구현한 것으로 간주하지 않는다.

 

위의 예처럼 인터페이스의 이름을 접두사로 붙여서 메서드를 정의하면 명시적 인터페이스 구현 메서드(Explicit Interface Method Implementation, EIMI)를 만들게 된다. 이러한 명시적 인터페이스 메서드는 public이나 pricate 같은 접근자를 설정할 수 없다.  이것은 컴파일러가 이 메서드의 메타데이터를 만들때 접근자를 private로 설정한 것으로 간주하여 클래스의 인스턴스를 통해 인터페이스의 메서드를 호출할 수 없도록 하기 위함이다. 

 


 

만약 같은 메서드 이름과 원형을 가지는 인터페이스들을 구현한다면 어떻게 될까

 

public interface IWindow
{
    Object GetMenu();
}

public interface IRestaurant
{
    Object GetMenu();
}

public class Base : IWindow, IRestaurant
{
    public Object GetMenu()
    {
        Console.WriteLine("Base's GetMenu()");
        return null;
    }
}

public static class Program
{
    public static void Main()
    {
        Base a = new Base();

        a.GetMenu();
        ((IWindow)a).GetMenu();
        ((IRestaurant)a).GetMenu();

        /*
         * 출력 :
         * Base's GetMenu()
         * Base's GetMenu()
         * Base's GetMenu()
         */
    }
}

 

Base의 GetMenu 메서드가 두 인터페이스의 GetMenu를 둘 다 구현한 것으로 간주한다.

 

따라서 각기 다른 기능을 구현하기 위해선 명시적 인터페이스 구현 메서드를 작성한 뒤 캐스팅을 수행하여 호출해야 한다.

 

명시적 인터페이스 구현 메서드로 타입 안정성 향상시키기

 

어떤 값 타입에서 IComparable을 구현한다고 해보자.

 

struct ValueType : IComparable
{
    private int m_x;
    public ValueType(int x) { m_x = x; }

    public int CompareTo(Object other) //인터페이스 구현
    {
        return m_x - ((ValueType)other).m_x;
    }
}

class Program
{
    public static void Main()
    {
        ValueType v = new ValueType(100);
        object o = new object();

        int n = v.CompareTo(v); //박싱 발생

        n = v.CompareTo(o); //런타임 중 InvalidCastException 발생
    }
}

위의 코드는 박싱이 발생하고 타입 안정성이 없다. 이런 문제점들은 명시적 구현을 통해 해결할 수 있다.

 

struct ValueType : IComparable
{
    private int m_x;
    public ValueType(int x) { m_x = x; }

    public int CompareTo(ValueType other) //인터페이스 구현
    {
        return m_x - ((ValueType)other).m_x;
    }

    int IComparable.CompareTo(object obj)
    {
        return CompareTo((ValueType)obj);
    }
}

class Program
{
    public static void Main()
    {
        ValueType v = new ValueType(100);
        object o = new object();

        int n = v.CompareTo(v);
        n = v.CompareTo(o); //컴파일 에러 발생
        n = ((IComparable)v).CompareTo(o); // Exception 발생
    }
}

이렇게되면 박싱이 발생하지 않고 컴파일 타임에 타입 안정성을 부여할 수 있다.

 

이러한 명시적 인터페이스 구현 메서드는 IConvertible, ICollection, IList, IDictionary... 등과 같은 인터페이스를 구현해야 하는 경우 자주 쓰게 된다.


 

주의

 

명시적 인터페이스 구현 메서드를 사용하면 부작용이 발생할 수 있으며 이런 부작용 때문에 명시적 인터페이스 구현 메서드를 사용하는 것은 최대한 자제해야 한다. (필요한 경우를 제외하고) 부작용은 다음과 같다.

 

  1. 명시적 인터페이스 구현 메서드를 타입 내에서 어떻게 구현하고 있는지 문서화 되어있지 않다.
  2. 값 타입의 인스턴스를 인터페이스로 캐스팅할 때 박싱이 발생한다.
  3. 명시적 인터페이스 구현 메서드는 상속한 타입해서 호출할 수 없다.
public static void Main()
{
    int x = 5;

    Single s = x.ToSingle(null); //컴파일 에러

    Single s2 = ((IConvertible)x).ToSingle(null); //OK.. 그러나 박싱 발생
}

1번 문제의 경우 위 코드를 보면 x는 ToSingle 메서드를 구현하고 있음에도 개발자가 이를 사용하기 위해서 어떻게 해야하는지 알기 어렵게 된다. 컴파일러는 단지 'int'타입에는 ToSingle() 메서드가 없다할 뿐이다.

 

2번처럼 인터페이스의 메서드를 사용하더라도 박싱을 해야만 한다는 문제가 있다.

 

 

class Base : IComparable
{
    int IComparable.CompareTo(object obj)
    {
        Console.WriteLine("Base's CompareTo()");
        return 0;
    }
}

class Derived : Base, IComparable
{
    public int CompareTo(Object obj)
    {
        return base.CompareTo(obj); //컴파일러 오류, Base에 CompareTo에 대한 정의가 없습니다.
    }
}

3번 문제의 경우 위와 같은 문제가 발생한다. 이 경우 가장 좋은 방법은 기본 클래스에서 명시적 인터페이스 구현 메서드 외에 추가적인 가상 메서드를 정의하고, Derived 클래스에서 이를 재정의하는 것이다.

 

class Base : IComparable
{
    public virtual int CompareTo(object o)
    {
        Console.WriteLine("Base's VIRTUAL CompareTo()");
        return 0;
    }

    int IComparable.CompareTo(object obj)
    {
        Console.WriteLine("Base's CompareTo()");
        return 0;
    }
}

class Derived : Base, IComparable
{
	//override가 붙었지만 IComparable의 메서드를 구현한 것으로 간주!!
    public override int CompareTo(Object obj) 
    {
        return base.CompareTo(obj); // OK
    }
}

 

 

 

기타


 

  • 인터페이스도 제너릭으로 구현할 수 있다. 메서드나 타입과 마찬가지로 박싱을 피할수 있고.. 웬만하면 이거 써라. 
  • 타입 상속은 IS-A, 인터페이스 구현은 CAN-DO 관계로 이해하면 쉽다.
  • 애매한 경우 타입은 모든 메서드를 구현할 필요는 없지만 인터페이스는 모든 메서드를 구현해야 한다는 점, 타입에는 메서드를 추가하면 끝이지만 인터페이스는 이 인터페이스를 구현한 모든 타입이 메서드를 구현해야 한다는 점을 고려하자.

 

'언어 > C#' 카테고리의 다른 글

C# - 배열  (0) 2021.02.10
C# - 문자열 - 1  (0) 2021.02.02
C# 제너릭 - 3  (0) 2021.01.20
C# - 제네릭 2  (0) 2021.01.18
C# 제너릭 - 1  (0) 2021.01.09