상속
- 부모 클래스의 멤버임을 명시하여 호출하는 방법 : base.를 붙인다.
- base : 데이터형 → 바로 위에있는 부모의 클래스
- C++과의 차이점 : C++은 부모클래스::로 명시하여 호출하였었다.
상속의 개념
- 상속(Inheritance)은 기존 클래스의 기능을 재사용하고 확장하는 메커니즘임.
- 하나의 클래스가 다른 클래스의 멤버들을 포함하게 됨.
- 중복되는 코드들을 줄일 수 있다.
- 가상함수로 인한 다형성을 확보할 수 있다.
- 가상 함수 : 부모 클래스의 함수를 재정의할 수 있게된다.
- 유지보수 및 유연성 있는 코드를 작성하려면 다형성은 꼭 필요하다.
- 상속받은 클래스는 기존 클래스를 기반으로 새로운 기능을 추가할 수 있음.
기본 문법
- 콜론(:)을 사용하여 상속을 표현함.
- 기반 클래스(Base class)의 모든 public/protected 멤버가 파생 클래스(Derived class)에 상속됨.
// 사용 예제
Stock msft = new Stock { Name="MSFT", SharesOwned=1000 };
Console.WriteLine(msft.Name); // MSFT
Console.WriteLine(msft.SharesOwned); // 1000
House mansion = new House { Name="Mansion", Mortgage=250000 };
Console.WriteLine(mansion.Name); // Mansion
Console.WriteLine(mansion.Mortgage); // 250000
// Asset 클래스 정의
public class Asset
{
public string Name;
}
// Asset을 상속받는 Stock 클래스
public class Stock : Asset // Asset을 상속함
{
public long SharesOwned;
}
// Asset을 상속받는 House 클래스
public class House : Asset // Asset을 상속함
{
public decimal Mortgage;
}
다형성(Polymorphism)
- 참조 변수는 다형성을 가짐.
- 기반 클래스 타입의 변수가 파생 클래스의 객체를 참조할 수 있음.
// 위의 Stock과 House 객체로 테스트
Stock msft = new Stock { Name="MSFT" };
House mansion = new House { Name="Mansion" };
Display(msft); // MSFT 출력
Display(mansion); // Mansion 출력
static void Display(Asset asset)
{
System.Console.WriteLine(asset.Name);
}
형변환과 참조 변환
업캐스팅(Upcasting) - 안전한 형변환
- 파생 클래스에서 기반 클래스로의 형변환을 업캐스팅이라고 함.
- 암시적(implicit) 변환이 가능함.
- 다형성에 의해 보장되기에 안전한 형변환이 된다.
- 위의 기반 클래스 타입의 변수가 파생 클래스의 객체를 참조할때 업캐스팅이 된다.
- 형변환이 되었더라도 동일한 객체 참조했다면 비교 연산을 할 수 있다.
Stock msft = new Stock();
Asset a = msft; // 업캐스팅 - 암시적 변환
Console.WriteLine(a == msft); // True - 동일한 객체 참조
다운캐스팅(Downcasting)
- 기반 클래스에서 파생 클래스로의 형변환을 다운캐스팅이라고 함.
- 명시적(explicit) 캐스팅이 필요함.
Stock msft = new Stock();
Asset a = msft; // 업캐스팅
Stock s = (Stock)a; // 다운캐스팅
Console.WriteLine(s.SharesOwned); // 정상 작동
Console.WriteLine(s == a); // True
Console.WriteLine(s == msft); // True
// 잘못된 다운캐스팅의 예
House h = new House();
Asset a2 = h; // 업캐스팅
Stock s2 = (Stock)a2; // 런타임 에러: InvalidCastException
as 연산자
- 안전한 캐스팅을 위해 사용됨.
- 캐스팅 실패시 null을 반환함.
- 형변환 받을 변수 as 형변환 할 데이터형
Asset a = new Asset();
Stock s = a as Stock; // s는 null - 예외 발생하지 않음
if (s != null)
Console.WriteLine(s.SharesOwned);
is 연산자
- 객체가 특정 타입과 호환되는지 검사함.
- 호환 결과를 bool 형으로 반환한다.
- C# 7.0부터는 패턴 매칭과 함께 사용 가능함.
Asset a = new Asset();
if (a is Stock)
Console.WriteLine(((Stock)a).SharesOwned);
// C# 7.0 이상에서는 다음과 같이 사용 가능
if (a is Stock s)
Console.WriteLine(s.SharesOwned);
가상 함수 멤버
- C#에서 함수 : 메서드, 생성자, 프로퍼티 등을 합쳐서 부른다.
- 즉, 가상 프로퍼티처럼 다 가상이 가능하다.
- virtual 키워드로 오버라이드 가능한 멤버를 선언함.
- override 키워드로 기반 클래스의 가상 멤버를 재정의함.
- override를 안해주면 상속받은 한수 정의대로 사용하게된다.
// 사용 예제
House mansion = new House { Name="McMansion", Mortgage=250000 };
Asset a = mansion;
Console.WriteLine(mansion.Liability); // 250000
Console.WriteLine(a.Liability); // 250000
public class Asset
{
public string Name;
public virtual decimal Liability => 0; // 가상 프로퍼티
}
public class Stock : Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage; // 재정의
}
추상 클래스와 추상 멤버
- abstract 키워드로 추상 클래스와 추상 멤버를 선언함.
- C++과 다른점은 =0이 없고 abstract만 적어도 추상 멤버가 된다.
- 추상 클래스는 인스턴스화(객체생성)할 수 없음.
- 추상 멤버는 구현을 제공하지 않으며, 파생 클래스에서 반드시 구현해야 함.
- C++과는 다르게 추상 메서드, 추상 프로퍼티 등 전부 재정의해줘야한다.
- 추상 함수 == 순수 가상 함수 라고 생각하면 된다.
- 다만, C#에서의 함수는 메서드만 있는게 아니라 프로퍼티나 생성자 등 도 있다.
- 추상 클래스 : 추상 멤버가 ‘하나라도’ 있다면 abstract로 선언해야한다.
public abstract class Asset
{
public abstract decimal NetValue { get; } // 추상 프로퍼티
}
public class Stock : Asset
{
public long SharesOwned;
public decimal CurrentPrice;
public override decimal NetValue => CurrentPrice * SharesOwned;
}
상속된 멤버 숨기기
- new 키워드를 사용하여 기반 클래스의 멤버를 의도적으로 숨길 수 있음.
- new를 안적어도 상속된 멤버를 숨길 수 있다.
- new를 쓴 이유 : 명시적으로 멤버를 숨기는것을 알리기 위해서다.
- 안써도 숨겨지기 때문에 문제를 확인하기 어렵다.
public class A { public int Counter = 1; }
public class B : A
{
public new int Counter = 2; // A의 Counter를 숨김
}
// 사용 예제
B b = new B();
A a = b;
Console.WriteLine(b.Counter); // 2
Console.WriteLine(a.Counter); // 1
new와 override 비교
- 멤버를 재정의하는 두 가지 방법이 있음:
- override: 가상 멤버를 재정의(실제 타입 기준으로 동작)
- new: 멤버를 숨김(참조 변수의 타입 기준으로 동작)
public class BaseClass
{
public virtual void Foo() { Console.WriteLine("BaseClass.Foo"); }
}
public class Overrider : BaseClass
{
public override void Foo() { Console.WriteLine("Overrider.Foo"); }
}
public class Hider : BaseClass
{
public new void Foo() { Console.WriteLine("Hider.Foo"); }
}
// 사용 예제
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // 출력: Overrider.Foo
b1.Foo(); // 출력: Overrider.Foo
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); // 출력: Hider.Foo
b2.Foo(); // 출력: BaseClass.Foo
함수와 클래스 봉인
- sealed 키워드를 사용하여 더 이상의 상속이나 오버라이드를 금지할 수 있음
- 클래스나 override된 함수에 적용 가능함
public class House : Asset
{
public decimal Mortgage;
public sealed override decimal Liability => Mortgage; // 더 이상 오버라이드 불가
}
public sealed class MyClass // 이 클래스는 상속할 수 없음
{
// ...
}
base 키워드
- base 키워드는 this 키워드와 유사하게 동작함
- 기반 클래스의 멤버에 접근할 때 사용함
- 기반 클래스의 생성자를 호출할 때도 사용함
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => base.Liability + Mortgage; // Asset의 Liability 호출
}
생성자와 상속
- 파생 클래스는 기반 클래스의 생성자를 직접 상속하지 않음
- 파생 클래스는 자신의 생성자를 정의해야 함
- base 키워드로 기반 클래스의 생성자를 호출할 수 있음
- 생성자의 호출은 부모 → 자식 순서로 진행된다.
public class Baseclass
{
public int X;
public Baseclass() { }
public Baseclass(int x) { this.X = x; }
}
public class Subclass : Baseclass
{
public Subclass(int x) : base(x) { } // 기반 클래스 생성자 호출
}
암시적 기반 클래스 생성자 호출
- 파생 클래스 생성자가 base를 명시하지 않으면 기반 클래스의 매개변수 없는 생성자가 자동으로 호출됨
public class BaseClass
{
public int X;
public BaseClass() { X = 1; }
}
public class Subclass : BaseClass
{
public Subclass() { Console.WriteLine(X); } // 출력: 1
}
생성자와 필드 초기화 순서
초기화는 다음 순서로 이루어짐:
- 파생 클래스에서 기반 클래스 방향으로:
- 필드가 초기화됨
- 기반 클래스 생성자 호출 인수가 평가됨
- 기반 클래스에서 파생 클래스 방향으로:
- 생성자 본문이 실행됨
public class B
{
int x = 1; // 3번째 실행
public B(int x)
{
// 4번째 실행
}
}
public class D : B
{
int y = 1; // 1번째 실행
public D(int x)
: base(x + 1) // 2번째 실행
{
// 5번째 실행
}
}
메서드 오버로딩과 해석
- 정적 해석: 오버로딩 해석은 매개변수의 정적 타입에 따라 결정됨.
- 구체성 규칙: 컴파일러는 타입 계층 구조를 기반으로 가장 구체적인 메서드를 선택함.
- 오버 로딩과 라이딩 차이점
- 로딩 : 컴파일 타임에 결정된다.
- 라이딩 : 런타임에 결정된다.
오버로딩과 구체성
static void Foo (Asset a) { }
static void Foo (House h) { }
- 컴파일러는 매개변수 타입에 따라 가장 구체적인 메서드를 선택함:
- House h = new House(...); Foo(h); // Foo(House)가 호출됨
- Foo(House)가 호출된 이유는 House 타입이 더 구체적이기 때문임.
컴파일 시간 해석
- 오버로딩 결정은 런타임 타입이 아니라 정적 타입에 따라 컴파일 시간에 이루어짐:
- Asset a = new House(...); Foo(a); // Foo(Asset)이 호출됨
- a의 런타임 타입이 House여도, 컴파일러는 a가 Asset으로 선언되었기 때문에 Foo(Asset)을 선택함.
Object Type
개요
- object(System.Object)는 모든 타입의 최상위 기본 클래스임
- 모든 타입은 object로 업캐스트 가능함
object 타입의 활용
- 범용적인 데이터 구조를 만들 때 유용함
- 예제: 일반적인 스택(Stack) 구현
public class Stack
{
int position;
object[] data = new object[10];
public void Push(object obj) { data[position++] = obj; }
public object Pop() { return data[--position]; }
}
- 사용 예시:
Stack stack = new Stack();
// 문자열 저장
stack.Push("sausage");
string s = (string)stack.Pop(); //다운캐스팅 후 호출
Console.WriteLine(s); // 출력: sausage
// 숫자 저장
stack.Push(3);
int three = (int)stack.Pop();
Console.WriteLine(three); // 출력: 3
박싱과 언박싱(Boxing & Unboxing)
박싱(Boxing)
- object형은 클래스이기에 ‘참조형 변수’가 생성되는 데 업캐스팅이 잘된다.
- 값 타입의 데이터를 참조 타입으로 변환하는 프로세스임
- 다음과 같은 처리가 일어남:
- 힙(heap) 메모리에 값을 저장할 객체가 생성됨
- 스택(stack)에 있던 값이 힙의 객체로 복사됨
- 객체(힙 영역 메모리)의 참조가 반환됨
- 즉, 박싱과 언박싱 중 3개의 int 메모리는 다 다른 메모리다.
박싱이 일어나는 상황들
// 1. 값 타입을 object 타입에 할당할 때
int i = 42; //statck 영역에 메모리 할당
object boxed = i; //박싱 발생 / i와는 다른 메모리가 된다.
// 2. 값 타입을 인터페이스 타입에 할당할 때
interface IDisplayable { }
struct Point : IDisplayable
{
public int X, Y;
}
Point p = new Point(); //구조체(값 형식)
IDisplayable d = p; // 박싱 발생
// 3. 값 타입을 파라미터로 전달할 때
Console.WriteLine(p); // ToString()을 호출하면서 박싱 발생
//Console.WriteLine(Point p); 로 넣으면 값형식 오버로딩된 메서드가 동작하게되어 박싱이 일어나지 않게된다.
- 값형 인스턴스는 값형식 전용 메서드를 만들어서 사용해야한다.
박싱의 특징
- 박싱된 값은 원본의 복사본임
- 원본 값을 변경해도 박싱된 값은 영향받지 않음
int i = 3;
object boxed = i;
i = 5;
Console.WriteLine(i); // 출력: 5
Console.WriteLine(boxed); // 출력: 3
언박싱(Unboxing)
- 박싱된 객체에서 값 타입을 추출하는 프로세스임
- 다음과 같은 처리가 일어남:
- 박싱된 객체(힙에 형변환 된 메모리)의 값이 스택으로 복사됨
- 값 타입 변수가 이 값을 받음
- 복사될때 기존의 stack 영역에 있는 메모리와는 다른 메모리가 된다.
언박싱 시 주의사항
- 반드시 명시적 캐스트가 필요함
- 원본 타입과 정확히 일치해야 함
- 타입이 일치하지 않으면 예외가 발생함
object boxed = 42; // int 값이 박싱됨
// 1. 올바른 언박싱
int i = (int)boxed; // 성공
// 2. 잘못된 타입으로 언박싱
try
{
//다운캐스팅때 기존의 데이터형과 다른 데이터형으로 바꾸게되면 에러가 발생한다.
long l = (long)boxed; // InvalidCastException 발생
}
catch (InvalidCastException e)
{
Console.WriteLine("잘못된 타입으로 언박싱 시도");
}
// 3. 올바른 타입 변환 방법
long l = (int)boxed; // 먼저 int로 언박싱
Console.WriteLine(l); // 그 다음 long으로 암시적 변환
박싱과 성능
- 박싱/언박싱은 추가적인 메모리와 CPU 사이클을 소비함
- 성능에 민감한 코드에서는 피하는 것이 좋음
- C#은 ‘가비지 컬렉터’가 메모리 관리를 한다.
- 박싱과 언박싱때 메모리가 100만번씩 생기기에 가비지 컬렉터가 100만번 이상 작동해야한다.
- C#은 ‘가비지 컬렉터’가 메모리 관리를 한다.
// 나쁜 예: 반복문에서 불필요한 박싱
int sum = 0;
for (int i = 0; i < 1000000; i++)
{
object boxed = i; // 박싱 발생
sum += (int)boxed; // 언박싱 발생
}
// 좋은 예: 박싱 없이 직접 처리
int sum2 = 0;
for (int i = 0; i < 1000000; i++)
{
sum2 += i; // 박싱/언박싱 없음
}
박싱을 피하는 방법
- 제네릭(Generic) 사용하기
- C++의 템플릿같은 역할이다.
- 값 타입 전용 메서드 오버로드 만들기
- 값형 인스턴스는 값형식 전용 메서드를 만들어서 사용해야한다.
- 필요한 경우에만 object로 변환하기
- 굳이 object형을 쓰지 않는것이 좋다.
- 박싱 언박싱을 자주하는 모습을 보인다면 ‘취직을 못할 수 있다.’
// 제네릭을 사용하여 박싱 피하기
public class GenericStack<T>
{
private T[] items = new T[100];
private int top = 0;
public void Push(T item)
{
items[top++] = item; // 박싱 발생하지 않음
}
public T Pop()
{
return items[--top]; // 언박싱 발생하지 않음
}
}
// 사용 예
var intStack = new GenericStack<int>();
intStack.Push(42); // 박싱 없음
int value = intStack.Pop(); // 언박싱 없음
GetType 메서드와 typeof 연산자
- 모든 타입은 런타임에 System.Type의 인스턴스로 표현됨
- 반환형이 System.Type이라는 의미이다.
- GetType(): 인스턴스의 타입 정보를 런타임에 확인함
- object 클래스에 선언되어있다. - 아무곳에서나 사용 가능하다.
- typeof(): 컴파일 타임에 타입 정보를 얻음
public class Point { public int X, Y; }
Point p = new Point();
Console.WriteLine(p.GetType().Name); // 출력: Point 데이터형의 이름
Console.WriteLine(typeof(Point).Name); // 출력: Point
Console.WriteLine(p.GetType() == typeof(Point)); // 출력: True
Console.WriteLine(p.X.GetType().Name); // 출력: Int32
Console.WriteLine(p.Y.GetType().FullName);// 출력: System.Int32
ToString 메서드
- object의 기본 메서드로 타입의 문자열 표현을 반환함
- 모든 내장 타입에서 재정의되어 있음
- 사용자 정의 타입에서도 재정의 가능함
public class Panda
{
public string Name;
public override string ToString() => Name;
}
Panda p = new Panda { Name = "Petey" };
Console.WriteLine(p); // 출력: Petey
object 클래스의 멤버들
public class Object
{
public Object();
public extern Type GetType();
public virtual bool Equals(object obj); //동등성 검사
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
public virtual int GetHashCode(); //해쉬테이블을 사용할때 사용자정의 클래스를 키 또는 Value로 쓸때 데이터형을 넘겨야할때 사용한다.
public virtual string ToString();
protected virtual void Finalize();
protected extern object MemberwiseClone();
}
구조체
구조체의 기본 개념
- 구조체(Struct)는 클래스와 유사하지만 다음과 같은 차이점이 있음:
- 구조체는 값 형식(Value Type)이고 클래스는 참조 형식(Reference Type)임
- 구조체는 상속을 지원하지 않음 (object 또는 System.ValueType으로부터 암시적 상속은 예외)
- 구조체는 다음 조건에서 사용하기 적합함:
- 값 형식의 의미론(semantics)이 필요한 경우
- 값 형식의 의미론 : 값형식의 데이터들의 집합이 필요한 경우
- 숫자 형식처럼 값의 복사가 자연스러운 경우
- 많은 인스턴스를 생성할 때 힙 할당을 줄이고 싶은 경우
- 값 형식의 의미론(semantics)이 필요한 경우
- C#에서의 구조체의 의미는 차이가 크다.
- 구조체 : 값 형식
- 변수를 생성하면 구조체의 참조가 아니라 ‘인스턴스’가 된다.
- C#은 구조체 상속이 안된다.
- C++은 된다.
- 부모클래스로 구조체를 못받는다는 소리다.
- 구조체 : 값 형식
- 클래스를 만들지 구조체를 만들지 선택을 먼저 해야한다.
- 수치 데이터가 많을 경우 - 구조체
- 구조체는 가비지 컬렉터와 관련이 없다.
구조체 선언과 사용
- 기본적인 구조체 예제:
struct Point
{
public int X, Y;
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
}
생성자 관련 규칙
- 구조체의 모든 필드는 생성자에서 명시적으로 할당되어야 함:
struct Point
{
int x, y;
// 정상 - 모든 필드 할당
public Point(int x, int y) { this.x = x; this.y = y; }
// 컴파일 오류 - y가 할당되지 않음
// public Point(int x) { this.x = x; }
}
기본 생성자와 초기화 순서
- 구조체는 항상 암시적인 매개변수 없는 생성자를 가짐
- 이 생성자는 모든 필드를 비트 단위로 0으로 초기화함
Point p = new Point(); // p.x와 p.y는 0이 됨
- 필드 초기화는 생성자 실행 전에 선언 순서대로 이루어짐:
struct Player
{
int shields = 50; // 먼저 초기화
int health = 100; // 다음 초기화
}
읽기 전용 구조체와 함수
- readonly 한정자를 사용하여 모든 필드가 읽기 전용이 되도록 할 수 있음:
readonly struct Point
{
public readonly int X, Y; // X와 Y는 읽기 전용이어야 함
}
- 구조체의 함수에도 readonly 한정자를 적용할 수 있음:
struct Point
{
public int X, Y;
// 컴파일 오류 - readonly 함수에서 필드 수정 불가
public readonly void ResetX() => X = 0;
}
ref Structs
- ref 한정자를 사용하여 구조체가 스택에만 존재하도록 강제할 수 있음
- 주로 성능 최적화를 위해 사용됨
// 힙에 할당될 수 없는 구조체
ref struct Point
{
public int X, Y;
}
// 컴파일 오류 - ref struct는 배열에 사용할 수 없음
// var points = new Point[100];
// 컴파일 오류 - ref struct는 클래스의 필드가 될 수 없음
class MyClass
{
// Point P;
}
- ref 구조체 제한사항:
- 힙에 할당될 수 있는 어떤 기능도 사용할 수 없음
- 람다식, 반복기, 비동기 함수에서 사용할 수 없음
- 인터페이스를 구현할 수 없음
- ref가 아닌 구조체 내부에 포함될 수 없음
액세스 한정자 (Access Modifiers)
개요
- 액세스 한정자(Access Modifier)는 타입이나 타입 멤버의 접근성을 제어하는 수식어임
- 캡슐화(Encapsulation)를 촉진하기 위해 타입이나 타입 멤버가 다른 코드에서 접근할 수 있는 범위를 제한함
액세스 한정자의 종류
- public
- 완전한 접근을 허용함
- 어떤 코드에서든 접근이 가능함
- 열거형(enum)이나 인터페이스(interface) 멤버의 기본 접근 수준임
- internal
- 같은 어셈블리 내에서만 접근 가능함
- 같은 어셈블리에서는 public이라고 생각하면 된다.
- 중첩되지 않은 타입의 기본 접근 수준임
- 같은 어셈블리 내에서만 접근 가능함
- private
- 포함된 타입 내부에서만 접근 가능함
- 클래스나 구조체 멤버의 기본 접근 수준임
- protected
- 포함된 타입과 상속받은 타입에서만 접근 가능함
- 다른 어셈블리의 파생 클래스에서도 접근 가능함
- protected internal
- protected와 internal의 결합임
- protected나 internal 중 하나라도 접근이 허용되면 접근할 수 있음
- 같은 어셈블리의 모든 코드와 다른 어셈블리의 파생 클래스에서 접근 가능함
- private protected (C# 7.2부터)
- protected와 internal의 교집합임
- 같은 어셈블리 내의 상속받은 타입에서만 접근 가능함
- 기준
- 1차 : 클래스 내외부
- 2차 : 자식 클래스 내부
- 3차 : 기타 등등
호출자의 위치 public protected internal protected internal private protected private file
| 파일 내 | ✔️️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| 클래스 내 | ✔️️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| 파생 클래스(동일한 어셈블리) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ |
| 비파생 클래스(동일한 어셈블리) | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ | ❌ |
| 파생 클래스(다른 어셈블리) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ |
| 비파생 클래스(다른 어셈블리) | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
예제
기본적인 액세스 한정자 사용
// Class2는 어셈블리 외부에서 접근 가능, Class1은 불가능
class Class1 {} // Class1은 internal (기본값)
public class Class2 {}
// ClassB는 x를 같은 어셈블리의 다른 타입에 노출, ClassA는 노출하지 않음
class ClassA { int x; } // x는 private (기본값)
class ClassB { internal int x; }
상속과 접근성
class BaseClass
{
void Foo() {} // Foo는 private (기본값)
protected void Bar() {}
}
class Subclass : BaseClass
{
void Test1() { Foo(); } // 오류 - Foo에 접근 불가
void Test2() { Bar(); } // 정상 - protected 멤버 접근 가능
}
접근성 제한(Accessibility Capping)
- 타입은 선언된 멤버의 접근성을 제한함
- 가장 일반적인 예는 internal 타입이 public 멤버를 가지는 경우임
class C { public void Foo() {} }
- C의 기본 접근성인 internal이 Foo의 접근성을 제한함
- 실질적으로 Foo는 internal이 됨
액세스 한정자 제약사항
- 기반 클래스의 함수를 재정의할 때는 동일한 접근성을 가져야 함:
class BaseClass { protected virtual void Foo() {} }
class Subclass1 : BaseClass { protected override void Foo() {} } // 정상
class Subclass2 : BaseClass { public override void Foo() {} } // 오류
- (다른 어셈블리에서 protected internal 메서드를 재정의할 때는 protected여야 함)
- 하위 클래스는 기반 클래스보다 더 접근성이 낮을 수는 있으나, 높을 수는 없음:
internal class A {}
public class B : A {} // 오류
인터페이스
인터페이스의 기본 개념
- 인터페이스는 클래스와 유사하나 동작만을 정의하고 상태(데이터)는 포함하지 않음
- 인터페이스의 특징:
- 함수만 정의할 수 있고 필드는 정의할 수 없음
- 인터페이스 멤버는 암시적으로 추상(abstract)임
- 클래스나 구조체는 여러 인터페이스를 구현할 수 있음
- 클래스는 하나의 클래스만 상속 가능하지만, 인터페이스는 여러 개 구현 가능함
- 인터페이스의 경우 ‘다중 상속’이 가능하다.
인터페이스 정의와 구현
- 인터페이스 선언은 클래스 선언과 유사하나 구현을 제공하지 않음