공부/C#

인터페이스, 인터페이스 상속, 열거형, 플래그 열거형, 중첩 타입

월러비 2025. 8. 13. 17:25

인터페이스

  • 사용자정의 데이터형 중 하나다.

인터페이스의 기본 개념

  • 인터페이스는 클래스와 유사하나 동작만을 정의하고 상태(데이터)는 포함하지 않음
  • 인터페이스는 ‘참조형’이다.
  • 인터페이스의 특징:
    • 함수만 정의할 수 있고 필드는 정의할 수 없음
    • 인터페이스의 모든 멤버는 암시적으로 추상(abstract)임
      • 상속을 받으면 반드시 재정의를 해야한다.
    • 클래스나 구조체는 여러 인터페이스를 구현할 수 있음
    • 클래스는 하나의 클래스만 상속 가능하지만, 인터페이스는 여러 개 구현 가능함

인터페이스 정의와 구현

  • 인터페이스 선언은 클래스 선언과 유사하나 구현을 제공하지 않음
  • 인터페이스는 접근지정자가 자동으로 public으로 지정된다.
  • 인터페이스를 상속받으면 인터페이스에 선언된 함수들은 전부 재정의해야한다.
  • 예제:
// IEnumerator 인터페이스 정의 예시
public interface IEnumerator
{
    bool MoveNext();
    object Current { get; } //오토 프로퍼티 (자동구현 프로퍼티)
    void Reset();
}

// 인터페이스 구현 예시
internal class Countdown : IEnumerator
{
    int count = 11;
    public bool MoveNext() => count-- > 0;
    public object Current => count;
    public void Reset() { throw new NotSupportedException(); }
}

  • 사용 예시:
IEnumerator e = new Countdown();
while (e.MoveNext())
    Console.Write(e.Current); // 109876543210 출력

인터페이스 상속

  • 인터페이스는 다른 인터페이스를 상속할 수 있음
  • 예제:
public interface IUndoable { void Undo(); }
public interface IRedoable : IUndoable { void Redo(); }

  • IRedoable을 상속받은 자식 클래스는 IUndoable의 멤버도 구현해야 함

명시적 인터페이스 구현

  • 여러 인터페이스를 구현할 때 멤버 시그니처가 충돌할 수 있음
  • 명시적 구현으로 해결 가능함
  • 인터페이스는 ‘다중 상속’이 가능하다.
  • 예제:
// 사용 예시
Widget w = new Widget();
w.Foo();                // Widget's implementation of I1.Foo
((I1)w).Foo();         // Widget's implementation of I1.Foo
((I2)w).Foo();         // Widget's implementation of I2.Foo

interface I1 { void Foo(); }
interface I2 { int Foo(); }

public class Widget : I1, I2 //인터페이스 다중 상속
{
    public void Foo() //I1 호출시 호출
    {
        Console.WriteLine("Widget's implementation of I1.Foo");
    }

    int I2.Foo()
    {
        Console.WriteLine("Widget's implementation of I2.Foo");
        return 42;
    }
}

인터페이스 멤버의 가상 구현

  • 암시적으로 구현된 인터페이스 멤버는 기본적으로 봉인(sealed)됨
  • virtual이나 abstract로 표시해야 재정의 가능함
  • 예제:
// 사용 예시
RichTextBox r = new RichTextBox();
r.Undo();               // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo();   // RichTextBox.Undo

public interface IUndoable { void Undo(); }

public class TextBox : IUndoable
{
    public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}

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

인터페이스 재구현

  • 하위 클래스에서 기본 클래스가 이미 구현한 인터페이스 멤버를 재구현할 수 있음
  • 예제:
// 사용 예시
RichTextBox r = new RichTextBox();
r.Undo();               // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo

public interface IUndoable { void Undo(); }

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

public class RichTextBox : TextBox, IUndoable
{
		//텍스트박스에서 넘어온 Undo 함수와, 인터페이스의 Undo 함수 2개가 있는것이다.
    public void Undo() => Console.WriteLine("RichTextBox.Undo");
}

인터페이스 재구현의 대안

  • 인터페이스 재구현에는 몇 가지 문제점이 있음:
    • 하위 클래스에서 기본 클래스 메서드를 호출할 방법이 없음
    • 기본 클래스 작성자가 메서드 재구현을 예상하지 못했을 수 있음
  • 더 나은 설계를 위한 두 가지 대안이 있음:
  1. 암시적 구현시 적절한 경우 virtual로 표시:
public class TextBox : IUndoable
{
    // virtual로 표시하여 오버라이드 허용
    public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}

  1. 명시적 구현시 다음 패턴 사용:
public class TextBox : IUndoable
{
    // 인터페이스 구현은 protected virtual 메서드 호출
    void IUndoable.Undo() => Undo();

    // 실제 구현은 protected virtual 메서드로
    protected virtual void Undo() => Console.WriteLine("TextBox.Undo");
}

public class RichTextBox : TextBox
{
    // 하위 클래스에서 오버라이드
    protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}

  • 하위 클래스화가 예상되지 않는 경우 sealed 키워드로 클래스를 봉인하여 인터페이스 재구현을 방지할 수 있음

인터페이스와 박싱

  • 구조체를 인터페이스로 변환하면 박싱(boxing)이 발생함
    • 인터페이스 : 참조형, 구조체 : 인스턴스형
    • 구조체 → 인터페이스 형변환이 박싱 위험이 있다.
  • 암시적으로 구현된 멤버를 호출할 때는 박싱이 발생하지 않음
  • 반대의 경우에도 ‘언박싱’이 발생한다.
  • 예제:
S s = new S();
s.Foo();    // 박싱 없음
I i = s;    // 인터페이스로 캐스팅할 때 박싱 발생
i.Foo();

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

기본 인터페이스 멤버 (C# 8.0 이상)

  • 인터페이스 멤버에 기본 구현을 추가할 수 있음
  • 인터페이스에서 구현된 함수는 상속받은 자식클래스에서 재정의하지 않았다면 자식클래스에서 이 함수를 사용하게 된다.
  • 예제:
interface ILogger
{
    void Log(string text) => Console.WriteLine(text);
}

class Logger : ILogger { }

// 사용 예시
((ILogger)new Logger()).Log("message");

// 정적 멤버도 가능
interface ILogger
{
    void Log(string text) =>
        Console.WriteLine(Prefix + text);
    static string Prefix = "";
}

// 외부에서 정적 멤버 접근
ILogger.Prefix = "File log: ";

열거형(enum)

  • 사용자 정의 데이터형
  • 값형식이다.
    • 구조체
    • 수치데이터형
    • 열거형 등

열거형의 기본 개념

  • 열거형은 명명된 상수값들의 집합을 정의하는 특별한 값 타입임
  • 열거형은 관련된 상수들을 그룹화하여 코드의 가독성과 유지보수성을 높여줌

열거형 정의 방법

  • 가장 기본적인 열거형 선언 방법:
public enum BorderSide { Left, Right, Top, Bottom }

  • 열거형 사용 예시:
BorderSide topSide = BorderSide.Top;
bool isTop = (topSide == BorderSide.Top); // true

열거형의 기본 특성

  • 기본적으로 정수형(int)을 기반으로 함
  • 열거형 멤버는 선언된 순서대로 0, 1, 2, ... 값이 자동 할당됨
  • 다른 정수 타입을 기반으로 하려면 명시적으로 지정 가능함:
    • 아래 예시는0은 0인데 byte 형이다.
    • int형과 차이점은 메모리크기이다.
public enum BorderSide : byte { Left, Right, Top, Bottom }
  • 명시적으로 값을 지정할 수 있음:
public enum BorderSide : byte
{
    Left=1,
    Right=2,
    Top=10,
    Bottom=11
}

열거형 값 변환

  • 열거형과 기반 정수 타입 간의 변환은 명시적 캐스팅이 필요함:
int i = (int)BorderSide.Left;
BorderSide side = (BorderSide)i;
bool leftOrRight = (int)side <= 2;
leftOrRight = side <= BorderSide.Right; //enum 끼리의 비교도 가능하다.

  • 다른 열거형 간의 변환도 가능함:
    • 해당 열거형이 가진 정수값을 갖게된다.
public enum HorizontalAlignment
{
    Left = BorderSide.Left,
    Right = BorderSide.Right,
    Center
}

HorizontalAlignment h = (HorizontalAlignment)BorderSide.Right;
h = (HorizontalAlignment)BorderSide.Right + 1; //이런것도 된다.

플래그(Flags) 열거형

  • 비트 플래그로 사용할 열거형은 [Flags] 어트리뷰트를 사용함
  • 멤버값은 보통 2의 제곱수로 지정함:
  • [Flags] : 어트리뷰트(attribute)
[Flags] 
public enum BorderSides
{
    None=0,
    Left=1,
    Right=1<<1, //2
    Top=1<<2,   //4
    Bottom=1<<3 //8
}

  • 비트 연산자를 사용하여 플래그 조합 가능:
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
//leftRight : 0000011 & 0000001 = 0000001
if ((leftRight & BorderSides.Left) != 0)
    Console.WriteLine("Includes Left"); // Includes Left 출력

string formatted = leftRight.ToString(); // "Left, Right" 출력

BorderSides s = BorderSides.Left;
s |= BorderSides.Right;
Console.WriteLine(s == leftRight); // True 출력

s ^= BorderSides.Right; // BorderSides.Right 토글
Console.WriteLine(s); // Left 출력

  • ^= → XOR 비교 연산자 ⇒ 서로 다를때 true , 같으면 false

추가, 삭제, 토글, 확인

  • OR 연산
  • And 연산
  • 토글
    • XOR 연산
  • 확인
    • 마스크 비트와 & 연산을 하고 결과를 마스크 비트와 == 비교를 하면 같은 비트인지 확인 가능하다.

열거형 연산자

  • 열거형에서 사용 가능한 연산자:
    • 할당 연산자: =
    • 비교 연산자: \==, !=, <, >, <=, >=
    • 산술 연산자: +, -
    • 비트 연산자: ^, &, |, ~
    • 복합 할당 연산자: +=, -=
    • 증감 연산자: ++, --
    • sizeof

타입 안전성 이슈

  • 열거형은 기반 타입으로의 캐스팅이 항상 가능하므로 유효하지 않은 값이 할당될 수 있음:
BorderSide b = (BorderSide)12345;
Console.WriteLine(b); // 12345 출력

  • 열거형 값의 유효성 검사를 위해 Enum.IsDefined 메서드 사용:
    • Enum.IsDefined(열거형 타입, 열거형이 열거형 선언 범위에 있는 값인지 확인용 변수)
BorderSide side = (BorderSide)12345;
Console.WriteLine(Enum.IsDefined(typeof(BorderSide), side)); // False 출력

  • 플래그 열거형의 유효성 검사:
bool IsFlagDefined(Enum e)
{
    decimal d;
    return !decimal.TryParse(e.ToString(), out d); //parse 가능하면 true , 못하면 false이니 유효한 값인지 확인 용도로 사용한다.
}

[Flags]
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }

// 사용 예시
for (int i = 0; i <= 16; i++)
{
    BorderSides side = (BorderSides)i;
    Console.WriteLine(IsFlagDefined(side) + " " + side);
}

중첩 타입(Nested Types)

  • 타입(클래스, 구조체, 인터페이스, 대리자, 열거형)은 클래스나 구조체 내부에 선언되어있다.

개요

  • 중첩 타입은 다른 타입 내부에 선언된 타입임
  • 타입(클래스, 구조체, 인터페이스, 대리자, 열거형)은 클래스나 구조체 내부에 중첩될 수 있음

중첩 타입의 특징

  • 중첩된 타입은 외부 타입의 private 멤버를 포함한 모든 멤버에 접근할 수 있음
  • 전체 범위의 접근 한정자(public, private, protected, internal) 사용 가능함
  • 기본 접근성은 private임 (일반 타입의 기본값인 internal과 다름)
  • 외부에서 접근할 때는 외부 타입의 이름으로 한정해야 함

코드 예제

기본적인 중첩 클래스

public class TopLevel
{
    public class Nested { } // 중첩 클래스
    public enum Color { Red, Blue, Tan } // 중첩 열거형
}

// 외부에서 중첩 타입 사용
TopLevel.Color color = TopLevel.Color.Red;

private 멤버 접근

public class TopLevel
{
    static int x;
    class Nested
    {
        static void Foo() { Console.WriteLine(TopLevel.x); }
    }
}

protected 접근 한정자 사용

public class TopLevel
{
    protected class Nested { }
}

public class SubTopLevel : TopLevel
{
    static void Foo() { new TopLevel.Nested(); }
}

외부 참조 예제

public class TopLevel
{
    public class Nested { }
}

class Test
{
    TopLevel.Nested n;
}

중요 참고사항

  • 중첩 타입은 컴파일러가 반복기나 익명 메서드와 같은 기능을 구현할 때 내부적으로 사용됨
  • 단순히 네임스페이스를 정리하기 위해서는 중첩 타입보다 중첩된 네임스페이스를 사용하는 것이 좋음
  • 중첩 타입은 외부 타입의 private 멤버에 접근해야 하거나, 강력한 접근 제어가 필요할 때 사용해야 함