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는 같은 파일내의 내부/파생 클래스에서만 접근을 허용한다.
메서드에서 기반 클래스 혹은 자신 클래스 인스턴스 가리키기
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를 이용하여 컴파일러가 타입을 결정하도록 한다.
- 명명된 튜플은 각각 필드의 이름을 지정하고 값을 대입할 수 있도록 한다.
- 튜플의 분해는 값이 저장된 튜플의 아이템을 각각 순서대로 매칭하여 대입한다.
- 튜플은 구조체이므로 대입은 깊은 복사가 일어난다.