인터페이스
인터페이스의 기본 개념
- 인터페이스는 클래스와 유사하나 동작만을 정의하고 상태(데이터)는 포함하지 않음
- 인터페이스는 ‘참조형’이다.
- 인터페이스의 특징:
- 함수만 정의할 수 있고 필드는 정의할 수 없음
- 인터페이스의 모든 멤버는 암시적으로 추상(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");
}
인터페이스 재구현의 대안
- 인터페이스 재구현에는 몇 가지 문제점이 있음:
- 하위 클래스에서 기본 클래스 메서드를 호출할 방법이 없음
- 기본 클래스 작성자가 메서드 재구현을 예상하지 못했을 수 있음
- 더 나은 설계를 위한 두 가지 대안이 있음:
- 암시적 구현시 적절한 경우 virtual로 표시:
public class TextBox : IUndoable
{
// virtual로 표시하여 오버라이드 허용
public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
- 명시적 구현시 다음 패턴 사용:
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 연산
- 토글
- 확인
- 마스크 비트와 & 연산을 하고 결과를 마스크 비트와 == 비교를 하면 같은 비트인지 확인 가능하다.
열거형 연산자
- 열거형에서 사용 가능한 연산자:
- 할당 연산자: =
- 비교 연산자: \==, !=, <, >, <=, >=
- 산술 연산자: +, -
- 비트 연산자: ^, &, |, ~
- 복합 할당 연산자: +=, -=
- 증감 연산자: ++, --
- 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 멤버에 접근해야 하거나, 강력한 접근 제어가 필요할 때 사용해야 함