[This is C#] Chapter 7. 클래스
Table of Contents
이것이 C#이다 책을 읽고 공부한 노트입니다.
객체지향 프로그래밍과 클래스 #
- 객체지향 프로그래밍(Object Oriented Programming; OOP)
- 문제 상황에 맞는 데이터형(클래스)을 만들어서 프로그래밍하는 방법이다.
- 클래스가 객체를 만들기 위한 청사진이라면, 객체는 그 실체(Instance)라고 볼 수 있다. 따라서 객체를 인스턴스라고 부르기도 한다.
- 클래스는 복합 데이터 형식이다.
- 객체는 속성(데이터), 기능(메소드)으로 이루어져 있다.
클래스의 선언과 객체의 생성 #
class Cat
{
public string Name;
public string Color;
public void Meow()
{
Console.WriteLine("{0} : 야옹", Name);
}
}
class MainApp
{
static void Main(string[] args)
{
Cat kitty1 = new Cat(); // Cat 객체를 생성한다.
kitty1.Name = "키티";
kitty1.Color = "하얀색";
Cat kitty2; // null을 가진다.
}
}
new
연산자와 생성자는 모든 데이터 형식에 사용할 수 있다.int a = new int(); // (O)
생성자와 종료자 #
생성자 #
- 형식
- 클래스와 이름이 같고, 반환 형식이 없다.
- 정의
- 명시적으로 생성자를 정의하지 않아도 컴파일러에서 생성자를 만들어 준다.
- 하지만 하나라도 생성자를 직접 정의하면, 기본 생성자를 제공하지 않는다.
- 오버로딩이 가능하다.
class Cat
{
public string Name;
public string Color;
public Cat()
{
Name = "";
Color = "";
}
public Cat(string _Name, string _Color)
{
Name = _Name;
Color = _Color;
}
public void Meow()
{
Console.WriteLine("{0} : 야옹", Name);
}
}
종료자 #
- 형식
- 클래스 이름 앞에
~
를 붙인다. - 생성자와 달리 매개변수도 없고, 한정자도 없다.
- 클래스 이름 앞에
- 정의
- 오버로딩이 불가능하다.
- 종료자는 되도록 구현하지 않는 것이 좋다.
- CLR의 가비지 컬렉터가 객체가 소멸되는 시점을 판단해서 종료자를 호출해준다. 가비지 컬렉터가 우리보다 훨씬 똑똑하게 객체의 소멸을 처리할 수 있다.
- 종료자를 명시적으로 구현하면 가비지 컬렉터는 클래스의 족보를 타고 올라가서 객체로부터 상속받은
Finalize()
메소드를 호출한다. 이렇게 되면 성능 저하를 초래할 확률이 높아진다.
class Cat
{
public string Name;
public string Color;
public Cat()
{
Name = "";
Color = "";
}
public Cat(string _Name, string _Color)
{
Name = _Name;
Color = _Color;
}
~Cat()
{
Console.WriteLine("{0}, 잘가", Name);
}
public void Meow()
{
Console.WriteLine("{0} : 야옹", Name);
}
}
정적 필드와 메소드 #
- 정적(static) 필드, 메소드란?
- 필드나 메소드가 인스턴스가 아닌, 클래스 자체에 소속되도록 하는 것이다.
- 프로그램 전체에 걸쳐 하나밖에 존재하지 않는다.
- 인스턴스를 생성하지 않아도 호출이 가능하다.
class MyClass
{
public static int StaticVal;
public int Val;
public static void StaticFunc()
{
//...
}
public void Func()
{
//...
}
}
class MainApp
{
static void Main(string[] args)
{
// 인스턴스 없이 접근 가능한 static 필드와 메소드
MyClass.StaticVal = 1;
MyClass.StaticFunc();
// 인스턴스에 소속되는 필드와 메소드
MyClass mine = new MyClass();
mine.Val = 1;
mine.Func();
}
}
객체 복사하기 : 얕은 복사와 깊은 복사 #
- 얕은 복사(Shallow Copy)
- 클래스는 태생이 참조 형식이기 때문에 다음 코드와 같은 경우, 두 객체 모두 하나의 힙에 있는 데이터를 가리키게 되는 문제가 있다.
class MyClass
{
public int Val;
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass();
a.Val = 1;
MyClass copyA = a;
copyA.Val = 2;
Console.WriteLine(a.Val); // 2
}
}
- 깊은 복사(Deep copy)
- 우리가 직접 깊은 복사를 수행하는 코드를 만들어서, 다른 힙에 저장되도록 할 수 있겠다.
class MyClass
{
public int Val;
public MyClass DeepCopy()
{
MyClass newCopy = new MyClass();
newCopy.Val = Val;
return newCopy;
}
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass();
a.Val = 1;
MyClass copyA = a.DeepCopy();
copyA.Val = 2;
Console.WriteLine(a.Val); // 1
}
}
this 키워드 #
-
this
키워드- 객체 내부에서 자기 자신의 필드나 메소드에 접근할 때 사용한다.
-
this()
생성자- 자기 자신의 생성자를 가리킨다.
- 생성자에서만 사용될 수 있다.
class MyClass
{
private int a, b, c;
public MyClass()
{
this.a = 0;
}
public MyClass(int b) : this()
{
this.b = b;
}
public MyClass(int b, int c) : this(b)
{
this.c = c;
}
}
접근 한정자로 공개 수준 결정하기 #
- C#에서 제공하는 접근 한정자 6가지
접근 한정자 | 설명 |
---|---|
public |
클래스 내부/외부 모든 곳에서 접근할 수 있다. |
protected |
클래스의 외부에서는 접근할 수 없지만, 파생 클래스에서는 접근이 가능하다. |
private |
클래스의 내부에서만 접근할 수 있다. 파생 클래스에서도 접근이 불가능하다. |
internal |
같은 어셈블리에 있는 코드에서만 public 으로 접근할 수 있다. 다른 어셈블리에 있는 코드에서는 private 과 같은 수준의 접근성을 가진다. |
protected internal |
같은 어셈블리에 있는 코드에서만 protected 으로 접근할 수 있다. 다른 어셈블리에 있는 코드에서는 private 과 같은 수준의 접근성을 가진다. |
private internal |
같은 어셈블리에 있는 클래스에서 생속받은 클래스 내부에서만 접근이 가능하다. |
- 클래스의 멤버의 접근 한정자는 디폴트로
private
이다.
상속으로 코드 재활용하기 #
- 객체를 생성할 때
- 기반 클래스의 생성자 → 파생 클래스의 생성자
- 객체가 소멸될 때
- 파생 클래스의 소멸자 → 기반 클래스의 소멸자
- 매개변수를 가지는 기반 클래스의 생성자를 호출하는 방법?
base
키워드를 사용한다. 이것은 기반 클래스를 가리킨다.base
키워드로 기반 클래스의 필드나 메서드에 접근할 수 있다.
class BaseClass
{
public int BaseVal;
public BaseClass(int BaseVal)
{
this.BaseVal = BaseVal;
}
public void BaseMethod()
{
//...
}
}
class DerivedClass : BaseClass
{
public int DerivedVal;
// base()를 통해 기반 클래스의 생성자 호출가능.
public DerivedClass(int BaseVal, int DerivedVal) : base(BaseVal)
{
this.DerivedVal = DerivedVal;
}
public void DerivedMethod()
{
// base 키워드를 통해 기반 클래스의 필드나 메서드에 접근 가능.
int val = base.BaseVal;
base.BaseMethod();
}
}
- 상속이 불가능하도록 만드려면?
sealed
키워드를 사용한다.
sealed class BaseClass
{
//...
}
class DerivedClass : BaseClass // 컴파일 에러!
{
//...
}
기반 클래스와 파생 클래스 사이의 형식 변환, 그리고 is와 as #
- C#에서 형식 변환을 위한 연산자:
is
,as
(Dog)
같이 형식을 변환하는 것 보다는as Dog
과 같이 사용하는 것을 권장한다.as
는 참조 형식에 대해서만 사용이 가능하므로, 값 형식의 객체는 기존의 형식 변환 연산자를 사용해야 한다.
연산자 | 설명 |
---|---|
is |
객체가 해당 형식으로 변환 가능한지 검사해서 그 결과를 bool 값으로 반환한다. |
as |
객체를 해당 형식으로 변환한다. 만약 실패하면 null 을 반환한다. |
class Animal
{
}
class Dog : Animal
{
public void Bark()
{
}
}
class Cat : Animal
{
public void Meow()
{
}
}
class MainApp
{
static void Main(string[] args)
{
Animal ani = new Dog();
if (ani is Dog)
{
Dog dog = (Dog) ani;
dog.Bark();
}
Cat cat = ani as Cat;
if (cat != null)
{
cat.Meow();
}
}
}
오버라이딩과 다형성 #
- 다형성(Polymorphism)
- 객체가 여러 형태를 가질 수 있음을 의미한다.
- 오버라이딩(Overriding)
- 다형성을 실현하는 방법이다. 기반 클래스의 메소드를 파생 클래스에서 재정의하는 것을 말한다.
- 기반 클래스의 메소드가
virtual
키워드로 한정되어 있어야 한다. 그리고 파생 클래스의 메소드에서override
키워드로 한정하여 컴파일러에게 재정의하고 있음을 알린다. private
으로 선언한 메소드는 오버라이딩 할 수 없다.
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal");
}
}
class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Bark");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
class MainApp
{
static void Main(string[] args)
{
Animal ani = new Dog();
ani.MakeSound(); // Bark
ani = new Cat();
ani.MakeSound(); // Meow
}
}
메소드 숨기기 #
- 메소드 숨기기(Method Hiding)
- CLR에게 기반 클래스에서 구현된 버전의 메소드를 감추고, 파생 클래스에서 구현된 버전만 보여주는 것이다.
- 파생 클래스의 메소드에
new
키워드를 사용한다. - 오버라이딩과 다른 점이라면, 아래 코드처럼 메소드를 단순히 숨기기만 한다는 점이다.
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal");
}
}
class Dog : Animal
{
public new void MakeSound()
{
Console.WriteLine("Bark");
}
}
class MainApp
{
static void Main(string[] args)
{
Animal ani = new Dog();
ani.MakeSound(); // Animal
}
}
오버라이딩 봉인하기 #
- 메소드를 오버라이딩되지 않도록 봉인하기
sealed
키워드를 사용한다.- 기반 클래스에서
virtual
로 선언된 메서드를 상속하는 파생 클래스의 메서드만 가능하다.
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal");
}
}
class Dog : Animal
{
public sealed override void MakeSound() // sealed
{
Console.WriteLine("Bark");
}
}
class Bulldog : Dog
{
public override void MakeSound() // 컴파일 에러!
{
}
}
읽기 전용 필드 #
- 읽기 전용 필드
readonly
키워드를 사용한다.- 생성자 안에서 한 번 값을 지정하면, 그 후로는 값을 변경할 수 없다.
- 클래스나 구조체의 멤버로만 존재할 수 있다.
class MyClass
{
private readonly int val;
public MyClass()
{
val = 1; // 생성자에서는 readonly 초기화 가능
}
public void MyMethod()
{
val = 1; // 컴파일 에러!
}
}
중첩 클래스 #
- 중첩 클래스(Nested Class)
- 클래스 안에 선언되어 있는 클래스이다.
- 자신이 소속되어 있는 클래스의 멤버에 자유롭게 접근할 수 있다.
class OuterClass
{
private int val;
class NestedClass
{
public void DoSomething()
{
OuterClass outer = new OuterClass();
outer.val = 1; // private이라도 접근이 가능하다.
}
}
}
분할 클래스 #
- 분할 클래스(Partial Class)
- 클래스의 구현이 길어질 경우 여러 파일에 나눠서 구현할 수 있게 한다.
partial
키워드를 사용한다.
partial class MyClass
{
public void Method1() {}
public void Method2() {}
}
partial class MyClass
{
public void Method3() {}
public void Method4() {}
}
확장 메소드 #
- 확장 메소드(Extension Method)
- 기반 클래스의 기능을 확장한다.
- 이것은 기반 클래스를 물려받아서 파생 클래스를 만든 뒤 필드나 메소드를 추가하는 상속과는 다른 것이다.
- 확장 메소드를 사용하면,
string
클래스에 문자열을 뒤집는 기능을 넣을 수도 있고,int
형식에 제곱 연산 기능을 넣을 수도 있다.
- 선언 방법
- 선언하는 클래스와 메소드는
static
한정자로 수식해야 한다. - 확장 메소드의 첫번째 매개변수는 반드시
this
키워드와 함께 확장하고자 하는 클래스(형식)의 인스턴스여야 한다.
- 선언하는 클래스와 메소드는
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 MainApp
{
static void Main(string[] args)
{
int pow = 2.Power(4);
int a = 2;
pow = a.Power(4);
}
}
구조체 #
- 클래스와 구조체의 차이점
특징 | 클래스 | 구조체 |
---|---|---|
형식 | 참조 형식 | 값 형식 |
복사 | 얕은 복사 | 깊은 복사 |
인스턴스 생성 | new 연산자와 생성자 필요 |
선언만으로 생성 |
생성자 | 제한 없음 | 매개변수 없는 생성자는 선언 불가능 모든 필드를 초기화하지 않는 생성자는 선언 불가능 |
상속 | 가능 | 불가능 |
변경불가능 선언 | 불가능 | 가능(readonly ) |
읽기 전용 메소드 | 불가능 | 가능(readonly ) |
- 변경불가능(Immutable) 구조체
readonly
키워드를 사용한다.- 해당 구조체의 모든 필드가
readonly
로 선언되도록 강제한다.
readonly struct ImmutableStruct
{
public readonly int ImmutableField; // (O)
public int MutableField; // 컴파일 에러!
public ImmutableStruct(int val)
{
ImmutableField = val; // 생성자에서만 초기화 가능
}
}
- 읽기 전용 메소드
readonly
키워드를 사용한다.- 해당 메소드가 객체의 상태를 바꿀 수 없게 한다.
- 구조체에서만 가능하다.
struct MyStruct
{
public int Value;
public readonly void TryToChange() // readonly 메소드는 객체의 상태를 바꿀 수 없다
{
Value = 1; // 컴파일 에러!
}
}
튜플 #
- 튜플(Tuple)
- 여러 필드를 담을 수 있는 구조체이다.
- 구조체이므로 값 형식이다.
- 명명되지 않은 튜플(Unnamed Tuple)
- 필드의 이름을 지정하지 않은 튜플
- 명명된 튜플(Named Tuple)
- 필드의 이름을 지정한 튜플
// 컴파일러가 튜플의 모양을 보고 직접 형식을 결정하도록 var를 이용해서 선언한다.
// 명명되지 않은 튜플
var unnamedTuple = ("박찬호", 13);
Console.WriteLine($"{unnamedTuple.Item1}, {unnamedTuple.Item2}");
// 명명된 튜플
var namedTuple = (Name: "박찬호", Age: 13);
Console.WriteLine($"{namedTuple.Name}, {namedTuple.Age}");
// 튜플 분해하기
var (name, age) = namedTuple;
Console.WriteLine($"{name}, {age}");
// 튜플 분해 시 특정 필드 무시하기
var (n, _) = namedTuple;
Console.WriteLine($"{n}");
// 튜플 생성과 분해를 한 번에 하기 -> 여러 변수를 단번에 초기화
var (color, value) = ("White", 33);
Console.WriteLine($"{color}, {value}");
- 튜플의 분해
- 튜플은 분해자(Deconstructor)를 구현하고 있기 때문에 분해가 가능하다.
- 분해한 결과를
switch
문이나switch
식의 분기 조건에 활용할 수도 있다. 이것을 위치 패턴 매칭(Positional Pattern Matching)이라고 한다.
var alice = (Job: "학생", Age: 30);
var discountRate = alice switch
{
("학생", int n) when n < 18 => 0.2,
("학생", _) => 0.1,
("일반", int n) when n < 18 => 0.1,
("일반", _) => 0.05,
_ => 0
};