공부/C#

상속, 업캐스팅, 다운캐스팅, as 연산자, is 연산자, 가상함수, 추상 클래스, 추상 멤버, base 키워드, 생성자와 상속, Object Type, 박싱, 언박싱, GetType 메서드, typeof 연산자, 구조체, ref 구조체, 인터페이스 개념

월러비 2025. 8. 11. 17:24

상속

  • 부모 클래스의 멤버임을 명시하여 호출하는 방법 : 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
}

생성자와 필드 초기화 순서

초기화는 다음 순서로 이루어짐:

  1. 파생 클래스에서 기반 클래스 방향으로:
    • 필드가 초기화됨
    • 기반 클래스 생성자 호출 인수가 평가됨
  2. 기반 클래스에서 파생 클래스 방향으로:
    • 생성자 본문이 실행됨
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만번 이상 작동해야한다.
// 나쁜 예: 반복문에서 불필요한 박싱
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)이 필요한 경우
      • 값 형식의 의미론 : 값형식의 데이터들의 집합이 필요한 경우
    • 숫자 형식처럼 값의 복사가 자연스러운 경우
    • 많은 인스턴스를 생성할 때 힙 할당을 줄이고 싶은 경우
  • 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)임
    • 클래스나 구조체는 여러 인터페이스를 구현할 수 있음
    • 클래스는 하나의 클래스만 상속 가능하지만, 인터페이스는 여러 개 구현 가능함
      • 인터페이스의 경우 ‘다중 상속’이 가능하다.

인터페이스 정의와 구현

  • 인터페이스 선언은 클래스 선언과 유사하나 구현을 제공하지 않음