1) 데이터와 데이터를 다루는 함수를 같이 작성할 수 있게 한 것
2) 데이터 은닉 (Data Hiding)
public class Student
{
// 필드
private string name;
// 메소드
public void SayName()
{
Console.WriteLine($"I'm {name}.");
}
}
1) 부모 클래스의 코드를 물려받아 재사용할 수 있게 함
2) 부모와 자식
class Derived : Base
{
void Foo()
{
base._baseMember5 = 1;
}
}
3) 문법
class 자식 클래스의 식별자 : 부모 클래스의 식별자 { }
class Program
{
public static void Main()
{
Derived d = new Derived();
d.Foo();
}
}
class Base
{
public void Foo() => Console.WriteLine("Foo");
}
class Derived : Base
{
}
→ Derived 클래스에는 Foo()가 존재하지 않지만, 부모 클래스인 Base의 멤버를 물려받으므로 Foo를 호출할 수 있음
→ 비주얼스튜디오에서도 Foo는 Base의 메소드임을 확인할 수 있음
4) 생성 순서
: 자식 클래스 인스턴스를 생성하는 경우 부모 - 자식 순으로 생성자가 호출
class Program
{
public static void Main()
{
// 부모 타입의 생성자부터 차례대로 호출됨을 알 수 있다.
Base3 obj = new Base3();
}
}
class Base1
{
public Base1() => Console.WriteLine("Base1");
}
class Base2 : Base1
{
public Base2() => Console.WriteLine("Base2");
}
class Base3 : Base2
{
public Base3() => Console.WriteLine("Base3");
}
: 특정 클래스의 상속을 막고싶은 경우 사용할 수 있는 한정자
1) 같은 코드로 다른 동작을 수행하도록 하는 특성
2) 예시
: 캐릭터의 직업군에 따라 물리 데미지가 다른 경우
class Program
{
public static void Main()
{
Character warrior = new Character()
{
_type = Character.Type.Warrior,
_atk = 10
};
Console.WriteLine($"전사가 줄 수 있는 물리 데미지 : {warrior.GetDamage()}");
Character magician = new Character()
{
_type = Character.Type.Magician,
_atk = 10
};
Console.WriteLine($"마법사가 줄 수 있는 물리 데미지 : {magician.GetDamage()}");
}
}
public class Character
{
// 캐릭터의 직업군
public enum Type
{
Warrior,
Magician
}
// 직업군을 따로 데이터로 저장해야 한다.
public Type _type { get; set; }
public int _atk { get; set; }
public int GetDamage()
{
// 데미지를 계산하려면 아래와 같이 조건문을 사용해야 한다.
switch (_type)
{
case Type.Warrior:
return (int)Math.Round(_atk * 1.5);
case Type.Magician:
return (int)Math.Round(_atk * 0.8);
default:
return 0;
}
}
}
→ 직업군이 늘어날 때마다 캐릭터의 직업군 목록을 enum 형식으로 계속 추가해야하고, 각 경우의 case도 추가해야 함
class Program
{
public static void Main()
{
// 서로 다른 타입을 똑같이 다룰 수 있게 된다.
Character[] characters =
{
new Warrior() { Atk = 10 },
new Magician() { Atk = 10 }
};
for (int i = 0; i < characters.Length; ++i)
{
Console.WriteLine($"물리 데미지 : {characters[i].GetDamage()}");
}
}
}
abstract class Character
{
public int Atk { get; set; }
public abstract int GetDamage();
}
// 조건문도 필요 없다.
class Warrior : Character
{
public override int GetDamage() => (int)Math.Round(Atk * 1.5);
}
class Magician : Character
{
public override int GetDamage() => (int)Math.Round(Atk * 0.8);
}
→ 직업군 관련 데이터(enum)를 따로 관리하지 않아도 되며, 조건문 없이 어떤 직업군 캐릭터의 동작인지 명확히 알 수 있음
→ 각 직업군 타입의 인스턴스를 생성했을 때 같은 메소드를 호출해도 각 타입별 동작을 수행하게 됨 (다형성)
1) 업캐스팅 (Upcasting)
// 예시 1
Character[] characters =
{
new Warrior() { },
new Magician() { }
};
// 예시 2 - 확인을 위해 Character 정의 시 abstract 키워드 제외
Character character = new Character();
Warrior warrior = new Warrior();
character = warrior;
→ 업캐스팅 된 상태에서 자식 클래스의 멤버는 사용할 수 없음 (부모 클래스 타입이므로 부모 클래스의 멤버만 사용)
2) 다운캐스팅 (Downcasting)
// 예시 1
Warrior warrior = (Warrior)characters[0];
// 예시 2
Character character = new Character();
warrior = (Warrior)character; // 다운캐스팅 아님 그냥 명시적 형변환
1) 업캐스팅과 다운캐스팅은 상속-파생 관계가 아닌 경우 런타임 오류 발생
→ is 연산자와 as 연산자 사용하여 좀 더 안전한 설계
2) is 연산자
if(characters[0] is Warrior)
{
(Warrior)characters[0];
}
→ characters[0] is Character 도 true
3) as 연산자
Warrior warrior = characters[1] as Warrior;
Console.WriteLine(warrior?.Def);
→ characters[1]이 Warrior와 호환되지 않으므로 warrior는 null
→ null check에 걸려 이후 동작(Def 값을 받아옴)을 수행하지 않음
1) 자식 클래스에서 재정의(오버라이딩)할 수 있는 멤버
2) 메소드, 프로퍼티, 인덱서, 이벤트와 같은 함수들이 가상멤버가 될 수 있음
3) 다형성을 제공하기위한 핵심 기능
4) 자식 클래스에서 재정의(오버라이딩)하기 위해 가상함수의 반환타입 앞에 override 한정자를 붙임
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Shape.Draw()");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Circle.Draw()");
}
}
1) 더이상 재정의하길 원하지 않는 가상 멤버를 재정의할 때 sealed 키워드를 사용
public class Ellipse :Shape
{
public sealed ovveride void Draw() {}
}
public class Circle : Ellipse
{
public ovveride void Draw() { } // 불가, Ellipse.Draw는 재정의 못하도록 sealed
}
: 구현 세부 정보를 숨기는 일반적인 인터페이스를 정의하는 행위
1) abstract
2) 추상 클래스
3) 추상 멤버
public abstract class Shape
{
public abstract int Area { get; }
}
class Rectangle : Shape
{
public int Width { get; set;}
public int Height { get; set;}
public override int Area
{
get => Width * Height;
}
}
1) 동작을 정의하는 용도로 사용
2) 인스턴스를 만드는 것이 불가능
3) 모든 멤버가 추상 멤버로 구성
4) 다중 상속 가능
상속은 단순히 코드 재사용 목적으로만 사용해서는 안된다!
예를들어 Shape 클래스에 Draw() 메소드가 있을 때 Circle은 Shape보다 구체적인 개념으로, Shape를 상속받아 Draw를 재사용하는 자식 클래스가 될 수 있다.
하지만 Person 클래스도 Draw 기능이 필요하다는 이유만으로 Shape를 상속받을 수는 없다는 의미이다.
이럴 때 IDrawable 인터페이스를 만들어 Shape와 Person 클래스 내에서 구현하도록 할 수 있다.아래 예제를 살펴보자.
class Program
{
public static void Main()
{
Circle circle = new Circle();
circle.Draw();
}
}
interface IDrawable
{
void Draw();
}
public class Person : IDrawable
{
public void Draw() => Console.WriteLine("IDrawable.Draw() => Person");
}
public class Shape : IDrawable
{
public void Draw() => Console.WriteLine("IDrawable.Draw() => Shape");
}
실행 결과
굉장히 훌륭한 설명이군요