-
13장. 인터페이스CLR Via C# 2022. 2. 16. 19:35
클래스를 상속받으면 상위 클래스의 메서드의 원형과 구현부를 제공받는다.
인터페이스를 상속받으면 해당 인터페이스의 메서드 원형만 받을 뿐 구현은 하위 클래스에서 해줘야 한다.
인터페이스 정의하기
인터페이스에는 메서드 외에 이벤트, 매개변수 없는 속성, 인덱서 등이 포함될 수 있다. 이 모두는 결국 메서드를 지칭하기 위한 구문상 편의사항이기 때문에 가능하다. 하지만 인터페이스는 생성자 메서드를 정의할 수 없으며 인스턴스 필드들도 정의할 수 없다.
C#에서는 interface 키워드를 사용하여 새로운 인터페이스에 이름을 부여하고 인스턴스 메서드의 원형들을 포함할 수 있다. 관례상 인터페이스 타입의 이름 앞에는 보통 대문자 I를 붙여주어 소스 코드상에서 해당 타입이 인터페이스임을 쉽게 알아챌 수 있도록 한다.
인터페이스에 인터페이스를 상속할 수 있다. 이 상속은 기존의 상속과 달리 계약이라고 생각하면 편하다. 인터페이스에 상속된 인터페이스 또한 상속한 클래스에서 구현해줘야 한다
public interface ICol<T> : IEnum<T>{ ... } // ICol<T>를 상속받은 클래스는 ICol<T>는 물론이고 IEnum<T> 또한 구현해줘야 한다.인터페이스 상속하기
클래스와 다르게 인터페이스는 다중 상속이 가능하다. C# 컴파일러는 인터페이스 메서드를 구현할 때 public으로 선언하도록 요구하다. 인터페이스에 속한 메서드는 모두 가상메서드에 속한다. 따라서 virtual을 명시적으로 지정하면 오류가 발생한다. 그래서 하위 클래스에서 구현할 때도 override를 붙이지 않는다.
제네릭 인터페이스
제네릭 인터페이스를 이용하면 여러 이점이 있다.
- 타입 안정성 : 타입을 그대로 받아와 컴파일 시점에 타입 안정성이 보장된다.
- 성능 이점 : 불필요한 박싱이 일어나지 않아서 성능에 이점이 있다.
- 동일 인터페이스를 다른 타입 매개변수를 지정하여 여러번 구현 : 제곧내
제네릭과 인터페이스 제약조건
제네릭의 타입 매개변수를 인터페이스로 제약할 때의 이점은 다음과 같다.
첫 번째 이점은 단일 제네릭 타입 매개변수가 여러 인터페이스 타입을 동시에 구현하도록 제약할 수 있다는 점이다. 이렇게 하면, 여기에 지정되는 타입은 제약조건에 기술된 모든 인터페이스를 반드시 구현해야만 한다.
private static Int32 M<T>(T t) where T : IComparable, IConvertible { ... } private static void Test(){ Int32 x = 5; Guid g = new Guid(); M(x); // Int32 는 IComparable, IConvertible 인터페이스 모두 구현하므로 문제 없음 M(g); // Guid 타입은 IComparable 인터페이스만 구현하고 IConvertible은 구현하지 않으므로 컴파일 오류 발생! }두 번째 이점은 값 타입의 인스턴스를 전달할 때 발생하게 되는 박싱을 최소화할 수 있다는 것이다.
같은 메서드 이름과 원형을 가지는 여러 인터페이스 구현하기
여러 개의 인터페이스를 구현해야 하는 타입을 작성하다 보면, 구현해야 하는 인터페이스들 내의 메서드가 동일한 이름과 원형을 가지는 경우가 있다. 이 경우 두 개의 서로 다른 인터페이스에서 제공하는 메서드를 각기 구현하려면 명시적 인터페이스 구현 메서드(EIMI)를 작성해야 한다.
public sealed class MarioPizzeria : IWindow, IRestaurant{ Object IWindow.GetMenu() { ... } Object IRestaurant.GetMenu() { ... } public Object GetMenu() { ... } }명시적 인터페이스 구현 메서드에는 public이나 private를 지정할 수 없다. 이는 기본적으로 private을 설정한 것으로 간주된다. 이후 사용은 명시적으로 해당 인터페이스로 캐스팅해 메서드를 호출하면 된다.
명시적 인터페이스 구현 메서드로 컴파일 시 타입 안정성 향상시키기
마땅한 제네릭 인터페이스가 존재하지 않아 어쩔 수 없이 비제네릭 인터페이스를 사용해야할 경우가 있다. 이러한 비제네릭 인터페이스에서 정의하고 있는 메서드들이 매개변수나 반환 타입으로 System.Object를 사용하는 경우, 컴파일 시점의 타입 안정성을 잃게 되는 문제가 발생하고 박싱이 발생하게 된다. 명시적 인터페이스 구현 메서드를 사용하여 이러한 상황을 개선하는 방법을 살펴보자.
public interface IComparable{ Int32 CompareTo(Object other); } internal struct SomeValueType : IComparable { private Int32 m_x; public SomeValueType(Int32 x) { m_x = x; } public Int32 CompareTo(SomeValueType other){ return (m_x - other.m_x); } Int32 IComparable.CompareTo(Object other){ return CompareTo((SomeValueType)other); } } public static void Main(){ SomeValueType v = new SomeValueType(0); Object o = new Object(); Int32 n = v.CompareTo(v); // 박싱 없음 n = v.CompareTo(o); // 컴파일 시점 오류 }이렇게 구현해도 만약 인터페이스 타입의 변수를 사용하게 되면, 다시 컴파일 시점의 타입 안정성을 잃고 박싱도 일어난다.
명시적 인터페이스 구현 메서드에서 주의해야 할 부분
명시적 인터페이스 구현 메서드에서 가장 큰 문제점은 다음과 같은 사항들이 있다.
- 명시적 인터페이스 구현 메서드를 타입 내에서 실제로 어떻게 구현하고 있는지가 문서화되어 있지 않다. 또한 Visual Studio IntelliSense도 이를 지원하지 않는다.
- 값 타입의 인스턴스를 인터페이스로 캐스팅할 때 박싱이 발생한다.
- 명시적 인터페이스 구현 메서드는 상속한 타입에서 호출할 수 없다.
이는 명시적 인터페이스 구현 메서드는 구현한 클래스 인스턴스에서는 호출이 불가능하고 해당 인터페이스로 캐스팅 후 호출해야한다. 이 과정에서 박싱이 발생한다.
마지막 가장 심각한 문제는 명시적 인터페이스 구현 메서드를 상속 받은 클래스에서 호출할 수 없다는 것이다. (Method라는 명시적 인터페이스 구현 메서드가 있다고 하자.) base.Method(o) 를 하위 클래스에서 호출하면 Base에는 Method라는 정의가 없기 때문에 오류가 발생한다. 다음 방법을 통해 해결이 가능하다.
- 인터페이스로 캐스팅 후 Method를 호출하려면 하위 클래스에 인터페이스 상속을 없애야 한다.(무한 재귀에 빠지지 않기 위해서)
- 가상 메서드를 이용해 하위 클래스에서 상위 클래스의 가상 메서드를 호출한다.
'CLR Via C#' 카테고리의 다른 글
15장. 열거 타입과 비트 플래그 (0) 2022.02.23 14장. 문자, 문자열, 텍스트 사용하기 (0) 2022.02.21 12장. 제네릭(Generics) (0) 2022.02.12 11장. 이벤트 (0) 2022.02.07 10장. 속성 (0) 2022.02.02