C# 기초 정리: 클래스, 구조체, 인터페이스, 추상 클래스

Table of Content


 

생성자와 소멸자

  • 생성자를 하나라도 정의하면 C# 컴파일러는 기본 생성자를 제공하지 않음
  • 소멸자는 오버로딩 불가능, 한정자 지정 불가능, 호출 불가능
  • 소멸자는 구현하지 않는 것이 좋음. CLR(Common Language Runtime)이 알아서 객체를 수거해가기 때문.
class MyClass
{
    private int id;
    private string name;
 
    /* 기본 생성자: 매개변수가 하나도 없음 */
    public MyClass()
    {
        id = 1;
        name = "안녕요?ㅎ";
    }
 
    /* 생성자를 구현하면 C# 컴파일러는 기본 생성자를 제공하지 않으므로
     * 기본 생성자를 직접 구현해야 함 */
    public MyClass(int id, string name)
    {
        /* this: 자신의 필드를 가리키는 키워드 */
        this.id = id;
        this.name = name;
    }
 
    /* 소멸자 */
    ~MyClass()
    {
        Console.WriteLine("ID: {0}, 넌 이미 죽어있다.", id);
    }
 
    /* 메소드 */
    public void Print()
    {
        Console.WriteLine("ID: {0}, Name: {1}", id, name);
    }
}
 
class Program
{
    public static void Main(string[] args)
    {
        MyClass c1 = new MyClass(); // 기본 생성자를 이용한 객체 생성
        MyClass c2 = new MyClass(100, "김씨샵"); // 따로 구현한 생성자를 이용한 객체 생성
        
        /* 클래스 인스턴스 메소드 호출 */
        c1.Print();
        c2.Print();
 
        /* 프로그램이 끝나면 소멸자가 자동으로 호출됨 */
    }
}

/* 출력 결과: 
 * ID: 1, Name: 안녕요?ㅎ
 * ID: 100, Name: 김씨샵
 * ID: 100, 넌 이미 죽어있다.
 * ID: 1, 넌 이미 죽어있다. */

 

클래스 상속

  • 형식은 class 파생클래스 : 기반클래스
  • 클래스 상속을 막으려면 sealed 한정자로 클래스를 구현하면 됨
/* Base: 기반 클래스 */
class Base
{
    public void BaseMethod() { }
}
 
/* Derived: Base 클래스에 기반한 파생 클래스 */
class Derived : Base
{
    /* base(): 기반 클래스의 생성자 */
    public Derived() : base() { }
 
    public void DerivedMethod()
    {
        base.BaseMethod();
    }
}
 
/* sealed 키워드: 클래스 상속 방지 */
sealed class ForbiddenClass { }
 
/* 에러! 상속 불가 */
class DerivedClass : ForbiddenClass { }

 

접근 한정자

  • public: 클래스 내/외부 모두 접근 가능​
  • protected: 파생 클래스에서 접근 가능​
  • private: 클래스 내부에서만 접근 가능 (기본값)​
  • 그 외: internal, protected internal
/* 기반 클래스 */
class Seoul
{
    public int a;
    protected int b;
    private int c;
}
 
/* 파생 클래스 */
class Guro : Seoul
{
    public Guro()
    {
        /* base: 기반 클래스의 필드를 가리키는 키워드 */
        base.a = 1; // a는 public이므로 어디서나 접근가능
        base.b = 2; // b는 protected이므로 파생클래스에서 접근 가능
        base.c = 3; // 에러! c는 private이므로 Seoul에서만 접근 가능
    }
}
 
class Program
{
    public static void Main(string[] args)
    {
        Guro g = new Guro();
        Console.WriteLine(g.a); // a는 public이므로 어디서나 접근 가능
        Console.WriteLine(g.b); // 에러! b는 protected이므로 파생클래스 외에선 접근 불가능
        Console.WriteLine(g.c); // 에러! c는 private이므로 Seoul에서만 접근 가능
    }
}

 

기반클래스 – 파생클래스 간 형식변환

  • 파생클래스의 인스턴스는 기반클래스의 인스턴스로 사용 가능
  • 기반클래스의 인스턴스는 파생클래스 형식으로 형변환 가능
  • is: 객체형식을 확인하여 반환
  • as: 형식변환 실패시 객체 참조를 null로 만듦
class Seoul { }
class Guro : Seoul { }
 
class Program
{
    static void Main(string[] args)
    {
        Seoul s1 = new Seoul();
        s1 = new Guro(); // 파생 클래스의 인스턴스는 기반 클래스의 인스턴스로 사용 가능
        Seoul s2 = new Seoul();
        Console.WriteLine("s2는 Seoul 형식? {0}", s2 is Seoul); // 출력: True
        Console.WriteLine("s2는 Guro 형식? {0}", s2 is Guro); // 출력: False
 
        Guro g1 = (Guro)s1; // 형변환 연산자: 기반 클래스의 인스턴스를 파생클래스 형식으로 강제 형변환
        Guro g2 = s1 as Guro; // as 연산자: 형식변환 실패시 객체 참조를 null로 만듦
        Console.WriteLine("g2는 Seoul 형식? {0}", g2 is Seoul); // 출력: True
        Console.WriteLine("g2는 Guro 형식? {0}", g2 is Guro); // 출력: True
    }
}

 

다형성과 오버라이딩

  • 다형성: 하위 형식 다형성의 준말. 파생 클래스를 통해 다형성을 실현.
  • 오버라이딩
    • 클래스 간의 상속 관계에서 메서드의 동작부를 재정의 하는 것. 반환 형식은 같게.
    • 기반 클래스에서 상속받은 메소드를 파생 클래스에서 재정의
    • 기반 클래스에서 오버라이딩할 메소드를 virtual 키워드로 한정​
    • 파생 클래스에서 오버라이딩할 메소드 구현 시 override 키워드 사용​
    • 기반 클래스에서 private로 선언한 메소드는 오버라이딩 불가​능. 파생 클래스에서 보이지 않으므로.
class Seoul
{
    /* 파생 클래스에서 오버라이딩할 메소드: virtual 키워드로 구현 */
    public virtual void Clean()
    {
        Console.WriteLine("서울시청 청소");
    }
}
 
class Guro : Seoul
{
    /* 기반 클래스에서 상속받은 메소드 오버라이딩: override 키워드로 구현 */
    public override void Clean()
    {
        Console.WriteLine("구로구청 청소");
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        Seoul s = new Seoul();
        s.Clean();
 
        Guro g = new Guro();
        g.Clean();
    }
}

 

메소드 숨기기

  • 기반 클래스의 메소드를 숨기고 파생 클래스의 메소드를 새롭게 구현
  • 파생 클래스에서 new 한정자로 메소드를 구현
class Seoul
{
    public void Announce()
    {
        Console.WriteLine("서울시 공지사항");
    }
}
 
class Guro : Seoul
{
    /* new 키워드를 사용하여 기반 클래스의 메소드를 숨김 */
    public new void Announce()
    {
        Console.WriteLine("구로구 공지사항");
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        /* Guro의 Announce() 호출. Seoul의 Announce()는 숨겨짐 */
        Guro g = new Guro();
        g.Announce();
 
        /* Seoul의 Announce() 호출. Seoul의 Announce()는 숨겨지지 않음 */
        Seoul s = new Seoul();
        s.Announce();
    }
}

 

오버라이딩 봉인

  • 오버라이딩한 ​메소드를 sealed로 한정하면 이후 오버라이딩 불가​
  • 잘못된 오버라이딩으로 인해 오류발생 가능성이​ 있다면 상속을 사전에 막는 것이 좋음​
class Seoul
{
    /* 파생 클래스에서 오버라이딩할 메소드: virtual 키워드로 구현 */
    public virtual void Clean()
    {
        Console.WriteLine("서울시청 청소");
    }
}
 
class Guro : Seoul
{
    /* 오버라이딩한 ​메소드를 sealed로 한정하면 이후 오버라이딩 불가​ */
    public sealed override void Clean()
    {
        Console.WriteLine("구로구청 청소");
    }
}
 
class Sindorim : Guro
{
    /* 에러! 오버라이딩 불가능 */
    public override void Clean()
    {
        Console.WriteLine("신도림동 청소");
    }
}

 

중첩 클래스(Nested Class)

  • 클래스 안에 선언된 클래스​
  • 중첩 클래스(Nested Class)는 소속된 클래스(Outer Class) private 멤버에도 접근 가능​
    • → Outer Class에서 Nested Class 멤버엔 접근 불가​
  • 왜 쓰는가?
    • 클래스 외부에 공개하고 싶지 않은 형식을 만들 때​
    • 현재 클래스의 일부분처럼 표현할 수 있는 클래스를 만들 때​
/* 소속된 클래스(Outer Class) */
class OuterClass
{
    private int outerValue;
    public void ControlNested()
    {
        NestedClass nc = new NestedClass();
        nc.nestedValue = 1; // 에러 O: Outer -> Nested private 멤버 접근 불가
    }
 
    /* 중첩 클래스(Nested Class) */
    class NestedClass
    {
        private int nestedValue;
        public void ControlOuter()
        {
            OuterClass oc = new OuterClass();
            oc.outerValue = 1; // 에러 X: Nested -> Outer private 멤버 접근 가능
        }
    }
}

 

분할 클래스(Partial Class)

  • 클래스의 구현이 길어질 경우 여러 개의 파일로 구현 가능
  • partial 키워드 사용
partial class Seoul
{
    public void Clean()
    {
        Console.WriteLine("서울시청 청소");
    }
}
 
partial class Seoul
{
    public void Announce()
    {
        Console.WriteLine("서울시청 공지");
    }
}

 

확장 메소드(Extension Method)

  • 클래스를 인스턴스화하지 않고도 해당 클래스(형식)에서 바로 사용할 수 있는 메소드
  • 구현 조건
    • 구현하려는 클래스를 static으로 한정​
    • 구현하려는 메소드를 static으로 한정​
    • 구현하려는 메소드의 첫번째 매개변수는 this 키워드를 사용하며,, 확장하고자 하는 클래스(형식)의 인스턴스를 지정
/* 확장 메소드를 포함하는 클래스: static으로 한정 */
public static class MyExtension
{
    /* 확장 메소드 조건
     * 1. static으로 한정
     * 2. 첫번째 매개변수는 this 키워드 사용, 
     *    확장하고자 하는 클래스(형식)의 인스턴스 지정 
     * 3. 두 번째 매개변수부터는 입력받을 매개변수 지정 */
    public static int Factorial(this int i)
    {
        if (i == 0 || i == 1)
            return 1;
        else
            return i * Factorial(i - 1);
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(15.Factorial()); // int 형식에 대한 확장 메소드 호출
    }
}

 

구조체(Structure)

  • 데이터를 담기위한 구조로 사용되므로 주로 public으로 구현
  • 클래스와의 차이점
    • 클래스는 참조 형식, 구조체는 값 형식
    • 클래스는 인스턴스를 new 연산자로 생성, 구조체는 선언만으로 생성
    • 클래스는 매개변수 없는 생성자 선언 가능, 구조체는 매개변수 없는 생성자 선언 불가
    • 클래스는 상속 가능, 구조체는 System.Object 형식을 상속하는 System.ValueType으로부터 직접 상속받음
/* 구조체 */
struct Point2D
{
    public int x;
    public int y;
 
    /* 매개변수 없는 구조체의 생성자는 선언 불가 */
    public Point2D(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
 
    /* System.Object 형식의 ToString() 메소드 오버라이딩 */
    public override string ToString()
    {
        return string.Format("({0}, {1})", x, y);
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        Point2D p1; // 구조체는 선언만으로도 인스턴스 생성
        p1.x = 1;
        p1.y = 2;
        Console.WriteLine(p1.ToString());
 
        Point2D p2 = new Point2D(10, 20);
        Point2D p3 = p2; // 구조체의 인스턴스를 다른 구조체 인스턴스에 할당하면 깊은 복사가 이루어짐
        Console.WriteLine(p2.ToString());
        Console.WriteLine(p3.ToString());
    }
}

 

인터페이스(Interface)

  • 구현부가 없는 메소드, 이벤트, 인덱서, 프로퍼티의 집합
  • 접근제한 한정자 사용 불가 (기본: public)​
  • 인터페이스를 상속받는 클래스는 인터페이스의 모든 메소드(및 프로퍼티)를 구현해야 함​
  • 관행적인 명명 규칙: 인터페이스 이름 앞에 ‘I’를 붙임​
  • 인터페이스의 인스턴스는 생성 불가능하지만 참조는 만들 수 있음​
/* 인터페이스 */
interface ILogger
{
    void WriteLog(string message);
}
 
/* 인터페이스를 상속받는 클래스 */
class ConosoleLogger : ILogger
{
    /* 인터페이스로부터 상속받은 메소드 구현 */
    public void WriteLog(string message)
    {
        Console.WriteLine("Log: {0}", message);
    }
}

 

인터페이스를 상속받는 인터페이스

interface ILogger
{
    void WriteLog(string message);
}
 
/* ILogger 인터페이스를 상속받은 인터페이스 */
interface IFormattableLogger : ILogger
{
    /* 이 인터페이스는 2개의 WriteLog() 메소드를 가짐 */
    void WriteLog(string format, params Object[] args);
}

 

여러 개의 인터페이스를 상속받는 클래스

interface IDog
{
    void Bark(string s);
}
 
interface ICat
{
    void Meow(string s);
}
 
/* 여러 개의 인터페이스를 상속받는 클래스 */
class Mammal : IDog, ICat
{
    public void Bark(string s)
    {
        Console.WriteLine(s);
    }
 
    public void Meow(string s)
    {
        Console.WriteLine(s);
    }
}

 

추상 클래스(Abstract Class)

  • abstract 한정자로 선언​
  • 특징​
    • 인터페이스적 특징: 인스턴스를 가질 수 없음, 추상 메소드를 지닐 수 있음​
    • 클래스적 특징: 메소드 구현 가능​
    • 필드, 메소드, 프로퍼티, 이벤트 기본 접근한정자는 private
    • 그러나 추상 메소드는 선언 시 private 외의 접근 한정자를 사용​해야 함
    • 추상클래스를 사용하면 다른 프로그래머가 파생클래스를 구현할 때​ 모든 추상 메소드를 구현해야 하는 사실을 잊어도 컴파일러가 그 사실을 상기시켜줌​ → 코드에 대한 매뉴얼 작성 부담이 줄어듦
/* 추상 메소드 */
abstract class MyAbstractClass
{
    private void Method() { } // 메소드
    public abstract void Method2(); // 추상 메소드
}
 
/* 추상 메소드를 상속받는 클래스 */
class MyClass : MyAbstractClass
{
    public override void Method2() { } // override 키워드를 사용하여 추상 메소드 구현
}

 

댓글 남기기