C#/C# 기본

[C#] 클래스

로파이 2021. 10. 6. 22:48

C# 클래스

클래스 선언

    class Cat
    {
        public string Name;
        public string Color;

        public void Meow()
        {
            Console.WriteLine("{0} : 야옹", Name);
        }

        // 기본 생성자
        public Cat()
        {
            Name = "미정";
            Color = "알 수 없음";
        }

        ~Cat()
        {
            Console.Write("안녕 {0}", Name);
        }
    }

필드(속성)과 메소드로 구성된다.

기본 생성자를 정의하지 않으면 컴파일러가 만들어 주며 소멸자는 객체가 소멸될 때 호출된다.

 

C# 클래스 인스턴스

        static void Example1()
        {
            // 모든 C# 클래스는 복합 데이터 형식으로 참조 형식이다
            // 힙 메모리에 개체를 동적 할당하고 그 메모리를 참조
            Cat kitty = new Cat();
            kitty.Name = "My Cat";
            kitty.Color = "White";
            kitty.Meow();

            // 참조 형식의 대입은 Shallow Copy가 이루어진다.
            Cat clone = kitty;
            clone.Name = "Clone Cat";
            kitty.Meow(); // Clone Cat
            clone.Meow(); // Clone Cat

            // 가비지 컬렉터
            // 키티가 먼저 파괴 ? 네로가 먼저 파괴 ?
            Cat nero = new Cat();
            nero.Name = "Nero";
            nero.Color = "Black";
            nero.Meow();
        }

인스턴스는 new를 이용하여 생성하며 동적 할당으로 초기화된 개체를 가르키는 참조 형식으로 초기화된다.

참조 형식을 사용하기 때문에 대입을 할 때 얕은 복사가 이루어진다.

 

- CLR의 가비지 컬렉터에 의한 개체 소멸

new를 통해 생성한 모든 개체의 메모리는 CLR (Common Language Runtime)에 의해 관리되며 참조가 0이 될 때 가비지 컬렉터에 의해 소멸된다. 가비지 컬렉터의 효율적인 수거를 위해 시간을 두었다 한꺼번에 처리하는 방식으로 실제 소멸이 지연될 수 있으며 어떤 개체가 먼저 소멸될지도 알 수 없다.

소멸자를 명시적으로 정의하면 내부에서 Finalize() 메서드를 호출하려하고 이는 성능 저하로 이어질 수 있다.

 

정적 필드와 메서드

static 키워드가 붙은 필드와 메서드는 클래스 이름으로 접근 가능하다. (인스턴스화 필요 X)

        class MyClass
        {
            public static int a;
            public static void StaticMethod()
            {

            }
        }
        
        static void Main(string[] args)
        {
            MyClass.a = 1;
            MyClass.StaticMethod();
            Console.ReadKey();
        }

 

접근 한정자를 이용한 상속

C#에서는 각각 필드와 메서드마다 한정자를 사용하여 접근 수준을 설정할 수 있게 되어있다.

접근 한정자 설명
public 클래스 내/외부 모든 곳에서 접근가능하다.
protected 클래스 내부/상속된 클래스에서 접근가능하다.
private 클래스 내부에서만 접근가능하다.
internal 같은 파일(어셈블리)에 정의된 코드에서 public으로 접근 가능하며 외부 파일에서는 private으로 취급된다.
protected internal 같은 파일(어셈블리)에 정의된 코드에서 public으로 접근 가능하며 외부 파일에서는 protected로 취급되어 상속된 클래스에서만 접근 가능하다.
private protected 같은 파일(어셈블리)에 정의된 내부/파생 클래스에서만 접근 가능하며 외부 파일에서는 private으로 취급된다.

C++과 다르게 internal이라는 키워드가 있고 같은 파일에서 public으로 접근 가능해진다. private protected는 같은 파일내의 내부/파생 클래스에서만 접근을 허용한다.

 

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers#summary-table

 

메서드에서 기반 클래스 혹은 자신 클래스 인스턴스 가리키기

base, this를 이용하여 기반/자신 클래스 인스턴스를 가리키거나 생성자를 호출할 수 있다.

    class Base
    {
        protected string Name;
        public Base(string Name)
        {
            this.Name = Name;
        }
    }

    class Derived : Base { 
        public Derived(string Name) : base(Name)
        {
            Console.WriteLine("{0}.Derived()", this.Name);
        }
    }

 

상속 금지된 클래스 sealed

    sealed class Base
    {
        protected string Name;
        public Base(string Name)
        {
            this.Name = Name;
        }
    }

    // 컴파일 에러
    class Derived : Base { 
        public Derived(string Name) : base(Name)
        {
            Console.WriteLine("{0}.Derived()", this.Name);
        }
    }

 

다형성 연산자 is와 as

C#에서는 두 인스턴스가 같은 클래스인지 판별하는 is와 Derived as Base로 사용되어 기반 클래스로 캐스팅이 가능한지 판별하는 as가 있다.

   class Soley 
   {
        public Soley()
        {
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Derived pD = new Derived("파생 개체");
            Soley pC = new Soley();

            // 컴파일 에러
            Base pA = pC as Base;

            Console.ReadKey();
        }
    }
  • is는 같은 형식일 때만 true(bool)을 반환한다.
  • as의 경우 캐스팅 실패시 null을 반환한다.

 

가상 함수와 오버라이딩

C#에서 가상 함수를 사용하려면 virtual 키워드를 메서드에 선언하고 파생 클래스에서 오버라이딩을 하려면 override 키워드를 붙여야한다. C#에서 오버라이딩을 할 때 접근 한정자를 파생클래스에서 변경하는 것이 불가능하다.

    class Base
    {
        protected string Name;
        public virtual void Init()
        {
            Console.WriteLine("Base Init");
        }
    }

    // 컴파일 에러
    class Derived : Base 
    { 
        public override void Init()
        {
            base.Init();
            Console.WriteLine("Derived Init");
        }
    }

- new 한정자로 메서드를 숨기기

가상 함수는 실제 형식의 오버라이드된 메서드가 호출되지만 new로 한정된 메서드는 가리키는 형식의 메서드가 호출된다.

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

        public virtual void Start()
        {
        }
    }

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

    class Program
    {
        static void Main(string[] args)
        {
            Base pD = new Derived();
            pD.Init(); // Base Init();
            pD.Start(); // Derived Start();

            Console.ReadKey();
        }
    }

 

- sealed 한정자로 메서드 봉인하기

C++ final 키워드와 같다. 오버라이딩이 불가능해진다.

 class Base
    {
        public virtual void Start()
        {
        }
    }

    class Derived : Base 
    { 
        public sealed override void Start()
        {
        }
    }

    class Finalized : Derived
    {
        public override void Start() // 메서드가 봉인되어 있어 상속 불가
        {
        }
    }

 

읽기 전용 필드

- const 상수 키워드

일반 변수의 const 키워드를 붙이면 해당 변수는 프로그램 실행되기 전 초기화되어있다.

const double PI = 3.1415926539;

- 읽기 전용 필드 readonly

생성자에서만 값을 지정할 수 있으며 그 이후로 값을 변경하는 것이 불가능하다.

   class Config
    {
        private readonly string version;
        private readonly string target;

        Config()
        {
            version = "2.1";
            target = "VS2019";
        }

        void SetVersion(string v) { version = v; } // 읽기 전용 필드
    }

 

분할 클래스

클래스의 구현을 여러 개의 파일에 나누어 정의할 수 있다.

partial 키워드를 붙인다.

    partial class MyClass
    {
        public void Method1() { }
        public void Method2() { }
    }

    partial class MyClass
    {
        public void Method3() { }
        public void Method4() { }
    }

 

확장 메서드

정적 클래스와 함수로 선언하여 특정 클래스의 메서드를 호출하는 것처럼 흉내낼 수 있다.

- 선언

public static class 클래스_이름

{

   public static 반환_타입 메서드_이름(this 대상_형식 식별자, 매개 변수)

   {

   }

}

   public static class IntegerExtension
    {
        public static int Power(this int myInt, int exponent)
        {
            int result = myInt;
            for (int i = 1; i < exponent; ++i)
                result = result * myInt;
            return result;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int base_num = 10;

            Console.WriteLine("Power {0} of {1} : {2}", 2, base_num, base_num.Power(2));

            Console.ReadKey();
        }
    }

 

구조체

C# 복합 데이터 형식 중 구조체 형식은 하나의 데이터 형식으로써 사용되는 의미가 크다.

구조체 형식은 new로 생성하지 않아도 된다. 이 의미는 기본적으로 스택에 할당하는 지역 변수로 취급되며 값 형식을 갖기 때문에 깊은 복사가 이루어진다.

구조체도 클래스와 마찬가지로 필드와 메서드에 접근 한정자를 붙일 수 있다.

    struct Asset
    {
        public int type;
        public string basePath;
        public string resourceName;

        Asset(int iType)
        {
            type = iType;
            basePath = "";
            resourceName = "";
        }

        public override string ToString()
        {
            return $"Type : {type}, Path : {basePath}, Resource : {resourceName}";
        }
    }

구조체는 기본 생성자를 정의할 수 없으며 매개 변수를 갖는 생성자만 정의할 수 있다. 생성자를 정의하였다면 반드시 생성자 안에서 모든 필드의 값을 초기화해야한다.

기본 생성자는 CLR이 정한 기본 값으로 초기화한다.

 

readonly 키워드로 선언된 구조체

readonly 키워드를 필드에도 사용할 수 있으나 구조체에 붙이면 모든 필드를 readonly로 선언하여금 강제한다. readonly는 생성자로만 값을 초기화할 수 있다.

    readonly struct Color
    {
        public readonly byte R;
        public readonly byte G;
        public readonly byte B;
        public readonly byte A;

        Color(byte r, byte g, byte b, byte a)
        {
            R = r;
            G = g;
            B = b;
            A = a;
        }
    }

readonly 메서드  (C# 8.0 이상부터 사용가능한 문법)

readonly 메서드는 필드 값을 수정하지 못하는 const성 메서드이다.

        public readonly uint GetKey()
        {
            return ((uint)R << 12) | ((uint)G << 8) | ((uint)B << 4) | (uint)A;
        }

 

튜플

여러 필드를 담는 구조체를 간단하게 표현하는 것으로 구체적인 타입없이 사용가능한 특징이 있다. python의 튜플과 기능이 유사하다.

    class Program
    {
        static void Main(string[] args)
        {
            // 명명되지 않은 튜플
            var a = ("슈퍼맨", 9999);
            Console.WriteLine($"{a.Item1}, {a.Item2}");
            
            // 명명된 튜플
            var b = (name : "배트맨", Age : 37);

            // 분해
            var (name, age) = b;

            Console.WriteLine($"{name} : {age}");

            b = a; // 값의 대입 (깊은 복사)

            Console.ReadKey();
        }
    }
  • var를 이용하여 컴파일러가 타입을 결정하도록 한다.
  • 명명된 튜플은 각각 필드의 이름을 지정하고 값을 대입할 수 있도록 한다.
  • 튜플의 분해는 값이 저장된 튜플의 아이템을 각각 순서대로 매칭하여 대입한다.
  • 튜플은 구조체이므로 대입은 깊은 복사가 일어난다.

'C# > C# 기본' 카테고리의 다른 글

[C#] 프로퍼티  (0) 2021.10.08
[C#] 인터페이스와 추상 클래스  (0) 2021.10.07
[C#] 메서드  (0) 2021.10.05
[C#] 분기문  (0) 2021.10.02
[C#] 연산자  (0) 2021.10.02