C# 클래스

2025. 2. 13. 21:17개발/C#

이 글은 박상현님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.


객체지향을 공부하면 가장먼저 직면하게 되는 요소는 바로 클래스일 것입니다. 처음 C++을 공부하며 클래스를 알게 됐을때, 함수 포인터를 가진 구조체라는 인상이었으며, "왜 클래스를 사용하는 것일까?" 생각했습니다. 객체지향은 개발의 능률을 높이기 위해 등장하였다고 읽었지만 스스로 느껴본 것이 없어서 그런지 굉장히 부자연스러웠습다. 하지만 개발의 규모가 커질 수록 객체지향의 코드가 객체의 관계로 더 이해하기 쉽고, 재사용성도 높아 코드의 유지보수에 탁월하든 것을 알게되었습니다. 또한 JAVA가 왜 가장 사랑받는 언어인지 이해 할 수 있었습니다. 당연히 완전 객체지향 언어인 C#도 클래스가 존재합니다.

 

클래스

class MyClass
{
    private string Name;
    private string Phone;
    
    public void Introduce()
    {
    	Console.WriteLine("My name is {0}", Name);
    }
}

 

C++과 다르게 코드 가독성을 높이기 위해 한정자를 모든 필드와 메서드 앞에 기입한 것이 인상적입니다. 만약 클래스의 한정자가 없는 멤버는 자동적으로 private으로 취급됩니다. 그리고 객체를 만들려면 무조건  new 키워드를 통해 힙에 할당해야 합니다. 그리고 할당된 객체는 연결한 루트 오브젝트를 통해 사용됩니다.

MyClass obj = new MyClass();
MyClass obj(); // not working

위 예시의 obj가 루트 오브젝트입니다. 이 루트 오브젝트가 스택에서 해제되고 나면 GC는 추후 힙에 할당한 객체를 제거하게 됩니다. 그래서 GC는 루트 오브젝트들의 리스트를 가지며 이 리스트의 크기를 줄이는 것이 성능에도 영향을 끼칩니다. 

 

상속

class Base
{
    public Base()
    {
        Console.WriteLine("Hello");
    }

    public void BaseMethod()
    {
        Console.WriteLine("Base Method");
    }
}

class Derived : Base
{
    public Derived(): base()
    {
        Console.WriteLine("World");
    }
}

상속을 하기 위해서는 파생클래스 뒤에 기반 클래스를 명시합니다. C++과 다르게 한정자는 들어가지 않습니다.  그리고 생성자 이니셜라이져를 통해 기반 클래스의 생성자를 사용할 수 있으며, 이 때 키워드는 base를 사용합니다. 

 Base obj = new Derived();
 obj.BaseMethod();
Hello
World
Base Method

다형성을 통해 기반 클래스의 루트 오브젝트로 파생 객체를 사용할 수 있습니다. 만약 유지보수를 위해 기반 클래스의 파생클래스를 만들지 않게 하려면 sealed 키워드를 사용하여 클래스를 봉인할 수 있습니다.

sealed class Base
{
    // ...
}

is as

두 연산자는 상속에 따른 형 변환의 안정성을 보장합니다.

class Animal
{

}

class Dog: Animal
{

}

위 예시는 상속에 자주 사용되는 관계인 AnimalDog입니다. 위 예시를 활용하여 isas의 사용해보겠습니다.

Animal	animal = new Dog();
Dog	dog;

if (animal is Dog)
{
    dog = (Dog)animal;
}

위 예시를 보면, is를 통해 판별을 하고 있습니다. is는 해당 루트 오브젝트가 가르키는 객체의 타입을 검사하여 bool 값으로 반환합니다. 즉 animal 루트 오브젝트의 객체가 Dog이면 명시적 형변환을 통해 Dog 타입의 루트 오브젝트로 연결하겠다는 것입니다.

Animal animal = new Dog();

Dog dog = animal as Dog;
if (dog != null)
{
    // The dog is doing something...
}

위 예시는 is 대신 as를 사용하여 형변환을 하였습니다. as는 명시적 형변환과 동일하게 작동하지만 다른 점은 예외를 던지지 않고 null을 반환한다는 것입니다. 그리고 null check를 통해 객체가 안전하게 형변환이 되었는지 확인할 수 있습니다.

ICloneable 

만약 객체를 복사한다면 깊은 복사와 얕은 복사 두가지 방식으로 나누어 집니다. 일반적으로 객체를 대입 연산자를 통하여 복사할 경우 깊은 복사가 아닌 앝은 복사가 진행되며, 해당 객체를 가르키는 또 다른 루트 오브젝트를 생성하게 됩니다. 객체의 깊은 복사의 경우 복사 생성자 혹은 ICloneable 인터페이스를 사용해야 합니다.

class MyClass
{
    public int Field1;
    public int Field2;
}
MyClass src = new MyClass();
src.Field1 = 10;
src.Field2 = 20;

MyClass dst = src;
dst.Field2 = 30;

위 예시는 앝은 복사를 나타냈습니다. 대입 연산자를 통해 같은 객체를 가르키는 두 개의 루트 오브젝트를 만들었습니다. 그래서 dst.Field2를 변경하였을 때 src.Field2 또한 30으로 변경되었습니다.

class MyClass: ICloneable
{
    public int Field1;
    public int Field2;
    
    public MyClass Clone()
    {
    	MyClass dst = new MyClass();
        dst.MyField1 = this.MyField1;
        dst.MyField2 = this.MyField2;
        return dst;
    }
}
MyClass src = new MyClass();
src.Field1 = 10;
src.Field2 = 20;

MyClass dst = (MyClass)src.Clone();
dst.Field2 = 30;

위 예시에서는 ICloneable 인터페이스를 상속받고 Clone 메서드를 작성 합니다. Clone 메서드는 new를 통해 새로운 객체를 할당하고 각 필드값을 복사합니다. 이 과정에서 서로 다른 객체를 가르키는 루트 오브젝트가 되며, 깊은 복사가 되었습니다. 주의해야 할 점은 새로 작성한 Clone 메서드의 반환타입은 MyClass 객체이지만 Object로 반환되기 때문에 명시적 형변환이 필요합니다. 이는 ICloneableClone 메서드 반환이 Object이기 때문입니다.

 

virtual

객체지향의 다형성은 하나의 루트 오브젝트를 통해 다양한 객체를 연결하는 것만은 아닙니다. 루트 오브젝트안의 객체가 달라지면 행동 또한 달라져야 합니다. virtual은 가상 테이블을 사용하여 동적 바인딩을 통해 올바른 메서드를 실행하게 합니다.

class Base
{
    public virtual void Show()
    {
        Console.WriteLine("Base::Show");
    }
}

class Derived : Base
{
    public override void Show()
    {
        Console.WriteLine("Derived::Show");
    }
}

C#에서는 가상 메서드를 오버라이딩할 메서드 앞에 override 키워드를 기입해야 합니다. C#의 가상 테이블은 CLR에 의해 클래스 단위로 관리되며 객체의 메서드 테이블 내에 존재하게 됩니다. 기반 클래스의 가상테이블은 파생 클래스에 상속되며 오버라이딩을 할 경우 그 기반 클래스의 가상 테이블에 있는 메서드를 파생 클래스 메서드로 덮어 쓰게 됩니다.

Base obj = new Base();
obj.Show(); // print: Base::Show

obj = new Derived();
obj.Show(); // print: Derived::Show

그래서 위 예시를 실행하면 첫 번째 코드는 Base 객체의 메서드가 실행되었지만 두번째 코드는 루트가 Base 이지만 출력은 파생클래스인 Derived의 메서드가 실행된 것을 확인 할 수 있습니다. 객체에 따른 동적 바인딩을 통해 메서드가 실행된 것입니다.

 

Method Hiding

메서드의 동적 바인딩이 아닌 정적 바인딩을 통한 실행방법도 있습니다.

class Base
{
    public virtual void Show()
    {
        Console.WriteLine("Base::Show");
    }
}

class Derived : Base
{
    public new void Show()
    {
        Console.WriteLine("Derived::Show");
    }
}

위 예시는 override 대신 new 키워드를 사용하여 메서드를 선언하였습니다. 여기서는 new는 객체를 생성할 때의 new와 비슷하면서도 다릅니다. 위 예시의 new는 해당 클래스의 메서드 테이블에 덮어 쓰는 것이 아닌 새롭게 메서드를 추가하는 것입니다.

Base obj = new Base();
obj.Show(); // print: Base::Show

obj = new Derived();
obj.Show(); // print: Base::Show

Derived dst = obj as Derived;
if (dst != null)
{
    dst.Show(); // print: Derived::Show
}

그래서 위 예시의 첫번째와 두번째 코드들은 실행하면 모두 Base의 메서드가 실행되지만, 세번째 코드는 Derived 루트를 사용하여 Derived의 메서드 테이블을 사용할 수 있게되며, 새롭게 추가한 Show 메서드를 실행시키며 Derived의 메서드가 실행된것을 볼 수 있습니다.

'개발 > C#' 카테고리의 다른 글

C# 대리자, 익명 메서드 그리고 람다식  (0) 2025.02.18
C# 추상 그리고 인터페이스  (2) 2025.02.14
C# 프로퍼티  (0) 2025.02.12
C# 종료자, Finalize, IDisposable  (0) 2025.02.12
C# 참조에 의한 매개변수 전달  (0) 2025.02.11