주녘공부일지

[C#] 인터페이스 ( interface ) 본문

C#/Definition, Etc

[C#] 인터페이스 ( interface )

주녘 2023. 10. 12. 19:18
728x90

1. 인터페이스의 상속

- 인터페이스는 다중 상속이 가능함

+ 다중 상속으로 인해 인터페이스의 멤버이름이 충돌할 수 있는데, 이를 해소하는 방법 중 하나는 멤버를 명시적으로 구현하는 것임 ( 후술 - 4번 )

interface I1 { ... }
interface I2 { ... }

public class A : I1, I2 { ... } // 다중 상속

- 인터페이스가 인터페이스를 상속받는 것도 가능함

 ex) IUndoable의 모든 멤버를 상속받는 IRedoable 인터페이스

public interface IUndoable             { void Undo(); }
public interface IRedoable : IUndoable { void Redo(); }

즉, IRedoable 인터페이스를 상속받는 클래스는 IUndoable의 멤버들도 반드시 구현해야 함 ( 구현 강제 - 형식 제공 )

2. 인터페이스의 멤버

인터페이스의 멤버는 암묵적으로 공용(public)이고 추상(abstract)임

- public 외의 접근 지정자를 따로 지정할 수 없음

- 추상 멤버만 가질 수 있음 ( 메서드, 속성, 이벤트, 인덱서 )

즉, 인터페이스를 상속 받은 클래스인터페이스의 모든 멤버에 대해 각각 public 구현을 강제

// using System.Collections로 사용할 수 있는 인터페이스
namespace System.Collections
{
    public interface IEnumerator
    {
         object Current { get; }
         bool MoveNext();
         void Reset();
    }
}

public class Countdown : IEnumerator
{
    int count = 11;
    
    // 인터페이스의 멤버들은 public으로 선언해야 함
    public object Current => count; 
    public bool MoveNext() => count-- > 0;
    public void Reset() { throw new NotSupportedException(); }
}

3. 인터페이스의 캐스팅

인터페이스를 구현하는 클래스의 객체는 상속하는 인터페이스로 캐스팅 할 수 있음

 ex) 위 예제의 Countdown 클래스를 IEnumerator로 캐스팅 // 외부에서도 공용으로 접근 가능

static void Main()
{
    // 인터페이스로 캐스팅
    IEnumerator e = new Countdown();
    while (e.MoveNext())
        Console.Write($"{e.Current} "); // 결과 : 10 9 8 7 6 5 4 3 2 1 0
}

구조체를 인터페이스로 캐스팅할 경우 박싱이 발생하기 때문에 지양해야 함

interface  I { void Foo(); }
struct S = I { public void Foo(){...} }

static void Main()
{
    S s = new S();
    s.Foo(); // 박싱 없음
    
    I i = s; // 구조체 -> 인터페이스로의 캐스팅에서 박싱 발생
    i.Foo();
}

4. 인터페이스의 명시적 구현 & 암묵적 구현

암묵적 구현

- public으로 선언되어 외부에서 객체를 통해 호출 가능

- virtual 키워드를 통해 재정의 가능

 

명시적 구현

- private으로 선언되어 외부에서 호출하기 위해서는 객체를 인터페이스 타입으로 업캐스팅 해야 함

- virtual 키워드를 통해 재정의 불가능

 

 ex) 인터페이스의 멤버이름이 충돌할 경우 해소하는 방법 - 명시적 구현

// 멤버 이름이 동일한 인터페이스 I1, I2
interface I1 { void Foo(); }
interface I2 { void Foo(); }

public class Widget : I1, I2
{
    // 암묵적 구현
    public void Foo() => Console.WriteLine("I1.Foo()");
    
    // 명시적 구현
    void I2.Foo() => Console.WriteLine("I2.Foo()");
}

static void Main()
{
    Widget w = new Widget();
    I2 i2 = w;
    
    w.Foo();       // I1.Foo()
    i2.Foo();      // I2.Foo()
    
    ((I1)w).Foo(); // I1.Foo()
    ((I2)w).Foo(); // I2.Foo()
}

5. 인터페이스의 가상 구현 (재정의)

virtual 키워드를 사용해 메서드를 재정의 할 수 있음

- 재정의 시에 인터페이스 캐스팅을 통해서도 재정의된 메서드로 호출됨

- base 키워드를 사용하여 확장할 수 있음

단, 명시적 구현된 메서드는 virtual 키워드를 사용할 수 없음

public interface IUndoable { void Undo(); }

public class TextBox : IUndoable
{
    // 가상함수로 선언
    public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}

public class RichTextBox : TextBox
{
    // override로 재정의 (확장 가능 - base 키워드)
    public override void Undo()
    {
        // base.Undo(); // base 키워드 사용 가능
        Console.WriteLine("RichTextBox.Undo");
    }
}

static void Main()
{
    RichTextBox r = new RichTextBox();
    
    r.Undo();               // RichTextBox.Undo
    ((TextBox)r).Undo();    // RichTextBox.Undo
    ((IUndoable)r).Undo();  // RichTextBox.Undo
}

6. 인터페이스의 재구현

기존 멤버 구현을 '하이재킹' 하는 것으로, 인터페이스를 상속받아 메서드를 재구현

( 단, 이는 대체로 일관성이 깨지는 것을 뜻하므로 바람직하진 않음 )

- 가상 함수, 명시적 구현, 암묵적 구현, 등의 영향을 받지 않음

- 명시적 구현되어 있는 경우에 가장 효과적

 

 ex) TextBox에 명시적 구현된 Undo() 메서드를 재정의하기 위해 IUndoable의 Undo() 메서드를 재구현하여 호출

public interface IUndoable { void Undo(); }

public class TextBox : IUndoable
{
    // 명시적 구현된 메서드
    void IUndoable.Undo() = Console.WriteLine("TextBox.Undo");
}

public class RichTextBox : TextBox, IUndoable // IUndoable
{
    // IUndoable의 Undo() 메서드를 재구현하여 재정의
    public void Undo() => Console.WriteLine("RichTextBox.Undo");
}

static void Main()
{
    RichTextBox r = new RichTextBox();
    r.Undo();                // RichTextBox.Undo
    ((IUndoable)r).Undo();   // RichTextBox.Undo
}

만약 위 예제의 TextBox 클래스가 Undo 메서드를 명시적으로 구현한 것이 아닌 암묵적 구현했다고 가정한다면, 아래 코드처럼 형식 체계를 깨뜨리는 코드가 가능

- 즉, 기반 클래스가 아닌 인터페이스를 통해 호출할 때만 효과적임

((TextBox)r).Undo(); // TextBox.Undo

이 외에도 다른 문제점들이 존재

1) 파생 클래스에서 기반 클래스의 메서드를 호출할 수 없음

2) 기반 클래스 작성자는 자신의 메서드가 재구현될 것임을 예상하지 못할 수 있으므로 재구현 시 문제가 발생할 가능성이 높아짐

 

즉, 멤버의 재구현은 명시적으로 구현된 인터페이스 멤버를 재정의하는 수단으로 사용하는 것이 가장 적합하며 파생을 염두에 두지 않고 기반 클래스를 파생하려 할 때의 마지막 수단으로 사용하는 것이 효과적이지만 가능한 재구현이 필요하지 않은 형태로 기반 클래스를 설계하는 것이 바람직함

인터페이스 재구현의 대안

- 인터페이스의 어떤 멤버를 암묵적으로 구현할 때에는 그 멤버를 virtual로 선언 (적합한 경우)

- 파생 클래스가 그 멤버를 임의의 논리로 재정의할 수도 있다고 예상할 때에는 아래와 같은 패턴을 적용

public class TextBox : IUndoable
{
    void IUndoable.Undo()         => Undo(); // 아래의 메서드를 호출
    protected virtual void Undo() => Console.WriteLine("TextBox.Undo");
}

public class RichTextBox : TextBox
{
    protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}

+ 더 이상의 파생이 없다고 예상되는 경우, sealed 한정자로 지정해 인터페이스의 재정의를 금지하는 것도 좋음

번외) 클래스와 인터페이스

클래스 : 구현을 공유하는 것이 자연스러운 형식에 사용

인터페이스 : 구현이 각자 독립적인 형식에 사용

 

+ 인터페이스의 이름은 대문자 I로 시작하는 것이 관례

 

참고도서) C# 6.0 완벽가이드

 

728x90