1. 소개
오늘 주제는 좀 추상적으로 느껴질 수도 있는데, 진짜 이거 없으면 앞으로 못 나가. 오늘은 상속에 대해 얘기할 거야. 오늘은 살짝 맛만 보고, 나중에 더 깊게 파헤칠 거야.
예를 들어, 너가 동물원 동물 관리 프로그램을 만든다고 해보자. 사자, 호랑이, 코끼리, 앵무새 등 여러 동물이 있어. 다 동물이긴 한데, 각자 특징도 있지:
- 사자는 갈기가 있어.
- 호랑이는 줄무늬가 있어.
- 코끼리는 엄청 크고 코가 길어.
- 앵무새는 말을 할 수 있어.
만약 각각의 동물을 따로따로 코드로 만든다면, 코드가 엄청 비슷해질 거야:
// 사자
public class Lion
{
public string Name { get; set; }
public int Age { get; set; }
public string Species { get; set; } = "사자"; // 항상 "사자"
public void Eat() { Console.WriteLine("사자가 고기를 먹는다."); }
public void Sleep() { Console.WriteLine("사자가 잔다."); }
public void Roar() { Console.WriteLine("사자가 포효한다!"); }
public string ManeColor { get; set; } // 사자만의 특징 - 갈기
}
// 호랑이
public class Tiger
{
public string Name { get; set; }
public int Age { get; set; }
public string Species { get; set; } = "호랑이"; // 항상 "호랑이"
public void Eat() { Console.WriteLine("호랑이가 고기를 먹는다."); }
public void Sleep() { Console.WriteLine("호랑이가 잔다."); }
public void Stride() { Console.WriteLine("호랑이가 살금살금 걷는다."); }
public string StripePattern { get; set; } // 호랑이만의 특징 - 줄무늬
}
코드 중복이 엄청 많지? Name, Age, Eat(), Sleep() — 이건 모든 동물에 공통이야! 만약 동물 종류가 100개라면? 끔찍하지!
여기서 상속이 빛을 발하는 거지.
2. C#에서 클래스 상속이란?
상속은 객체지향 프로그래밍(OOP)의 기본 원칙 중 하나야. 이미 존재하는 클래스를 기반으로 새로운 클래스를 만들 수 있게 해줘. 새로운 클래스(자식 클래스, 파생 클래스)는 부모 클래스(기본 클래스, 부모 클래스)의 모든 속성(필드)과 동작(메서드)을 물려받아.
현실에서도 비슷하지: 부모님한테 어떤 특징을 물려받지만, 너만의 고유한 특징도 있잖아.
C#에서 상속은 자식 클래스 이름 뒤에 콜론 :을 붙여서 표현해:
// 부모(기본) 클래스 - Animal
public class Animal
{
public string Name { get; set; } // 동물 이름
public int Age { get; set; } // 동물 나이
public void Eat()
{
Console.WriteLine($"{Name}가 먹는다.");
}
public void Sleep()
{
Console.WriteLine($"{Name}가 잔다.");
}
}
// 자식(파생) 클래스 - Lion은 Animal을 상속받음
public class Lion : Animal // <-- 이게 바로 상속!
{
public string ManeColor { get; set; } // 사자만의 속성
public void Roar() // 사자만의 동작
{
Console.WriteLine($"{Name}가 포효한다: 으르렁!");
}
}
// 또 다른 자식 클래스 - Tiger는 Animal을 상속받음
public class Tiger : Animal
{
public string StripePattern { get; set; } // 호랑이만의 속성
public void Stride() // 호랑이만의 동작
{
Console.WriteLine($"{Name}가 조용히 살금살금 걷는다.");
}
}
이제 Lion이나 Tiger 객체를 만들면, Name과 Age 속성, Eat()과 Sleep() 메서드를 자동으로 가지게 돼. 왜냐면 Animal에서 상속받았으니까!
class Program
{
static void Main(string[] args)
{
Lion simba = new Lion();
simba.Name = "심바";
simba.Age = 5;
simba.ManeColor = "골드";
simba.Eat(); // Animal에서 상속받은 메서드
simba.Sleep(); // Animal에서 상속받은 메서드
simba.Roar(); // Lion만의 메서드
Console.WriteLine($"{simba.Name} - 나이: {simba.Age}, 갈기 색: {simba.ManeColor}");
Tiger shereKhan = new Tiger();
shereKhan.Name = "셰르칸";
shereKhan.Age = 7;
shereKhan.StripePattern = "클래식";
shereKhan.Eat(); // Animal에서 상속받은 메서드
shereKhan.Sleep(); // Animal에서 상속받은 메서드
shereKhan.Stride(); // Tiger만의 메서드
Console.WriteLine($"{shereKhan.Name} - 나이: {shereKhan.Age}, 무늬: {shereKhan.StripePattern}");
}
}
상속의 핵심 아이디어:
- 코드 재사용성(Code Reusability): 공통 로직을 기본 클래스에 넣어서 코드 중복을 피할 수 있어.
- 계층 구조(Hierarchy): "일반-특수" 구조가 만들어져 (Animal → Lion/Tiger).
- 다형성(Polymorphism): (지금은 이름만 언급, 자세한 건 나중에!) 자식 클래스 객체를 부모 클래스 타입으로 다룰 수 있어. 예를 들어 Animal 리스트에 사자, 호랑이 다 넣고 Eat() 메서드를 호출할 수 있지.
3. 기본 클래스 생성자 호출 (base)
자식 클래스 객체(예: new Lion())를 만들면, C#은 자동으로 자식 클래스 생성자 전에 기본 클래스(Animal) 생성자를 호출해. 이 덕분에 부모 부분이 제대로 초기화되지!
가끔은 부모 생성자에 파라미터를 넘겨야 할 때가 있어. 이럴 때 자식 생성자 시그니처 뒤에 base 키워드를 써.
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
// 기본 클래스 생성자
public Animal(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine($"Animal 생성자: 동물 {Name} 생성, 나이 {Age}.");
}
public void Eat()
{
Console.WriteLine($"{Name}가 먹는다.");
}
public void Sleep()
{
Console.WriteLine($"{Name}가 잔다.");
}
}
public class Lion : Animal
{
public string ManeColor { get; set; }
// Lion 생성자에서 Animal 생성자 호출
public Lion(string name, int age, string maneColor) : base(name, age) // <-- 이게 바로 'base'!
{
ManeColor = maneColor;
Console.WriteLine($"Lion 생성자: 갈기 색 {ManeColor}.");
}
public void Roar()
{
Console.WriteLine($"{Name}가 포효한다: 으르렁!");
}
}
class Program
{
static void Main(string[] args)
{
// Lion을 만들면 먼저 Animal 생성자, 그 다음 Lion 생성자 호출됨
Lion simba = new Lion("심바", 5, "골드");
simba.Roar();
}
}
new Lion("심바", 5, "골드") 실행 시 일어나는 일:
- Lion(string name, int age, string maneColor) 생성자가 호출됨.
- : base(name, age) 덕분에 Animal(string name, int age) 생성자로 제어가 넘어감.
- Animal 생성자 코드가 실행되고, Animal 생성자: 동물 심바 생성, 나이 5.가 출력됨.
- 그 다음 제어가 Lion 생성자로 돌아옴.
- Lion 생성자 코드가 실행되고, Lion 생성자: 갈기 색 골드.가 출력됨.
이게 진짜 중요해: 부모 클래스가 항상 먼저 초기화된다!
4. is와 as 연산자
C# 프로그래밍 하다 보면, 객체의 타입을 검사하거나 다른 타입으로 변환해야 할 때가 많아. 이럴 때 is와 as 연산자가 등장하지! 상속, 다형성 쓸 때 특히 많이 쓰는데, 지금은 기본만 알아두자.
is 연산자 (타입 검사)
is 연산자는 객체가 특정 타입과 호환되는지 검사해. 변환 가능하면 true, 아니면 false를 반환하지.
문법: 표현식 is 타입
static void AnalyzeObject(object obj)
{
if (obj is string)
{
Console.WriteLine("이건 문자열이야!");
}
else if (obj is int)
{
Console.WriteLine("이건 정수야!");
}
else
{
Console.WriteLine("이건 다른 거야.");
}
}
static void Main()
{
AnalyzeObject("안녕, 세상!"); // 이건 문자열이야!
AnalyzeObject(123); // 이건 정수야!
AnalyzeObject(3.14); // 이건 다른 거야.
AnalyzeObject(new int[] { 1, 2 }); // 이건 다른 거야.
}
is와 패턴 매칭:
최신 C#에서는 is로 타입 검사하면서 동시에 변수 선언도 할 수 있어. 이걸 "패턴 매칭"이라고 하고, 코드가 훨씬 간결해져.
static void AnalyzeObjectWithPatternMatching(object obj)
{
if (obj is string message) // obj가 문자열이면 message에 할당
{
Console.WriteLine($"이건 문자열, 길이: {message.Length}");
}
else if (obj is int number) // obj가 int면 number에 할당
{
Console.WriteLine($"이건 숫자, 2배: {number * 2}");
}
else
{
Console.WriteLine("타입을 알 수 없음.");
}
}
static void Main()
{
AnalyzeObjectWithPatternMatching("예시"); // 이건 문자열, 길이: 2
AnalyzeObjectWithPatternMatching(50); // 이건 숫자, 2배: 100
AnalyzeObjectWithPatternMatching(new object()); // 타입을 알 수 없음.
}
is와 패턴 매칭 방식이, 옛날 방식(타입 변환 후 null 체크)보다 훨씬 안전하고 읽기 좋아.
as 연산자 (안전한 타입 변환)
as 연산자는 객체를 특정 타입으로 변환하려고 시도해. 변환 가능하면 변환된 객체를 반환하고, 불가능하면 null을 반환해(예외 안 던짐). 그래서 as는 "안전한" 타입 변환 연산자야.
문법: 표현식 as 타입
static void ProcessData(object data)
{
string text = data as string; // data를 string으로 변환 시도
if (text != null) // 변환 성공 여부 확인
{
Console.WriteLine($"문자열 처리: {text.ToUpper()}");
}
else
{
Console.WriteLine("데이터가 문자열이 아님.");
}
}
static void Main()
{
ProcessData("hello"); // 문자열 처리: HELLO
ProcessData(123); // 데이터가 문자열이 아님.
ProcessData(null); // 데이터가 문자열이 아님.
}
as를 직접 변환 (타입)표현식 대신 쓸 때:
- 안전성: 객체가 진짜 원하는 타입인지 확신이 없을 때, as를 쓰면 InvalidCastException 없이 null을 받아서 처리할 수 있어.
- 참조 타입만: as는 참조 타입(클래스, 인터페이스, 델리게이트, 배열)에만 쓸 수 있어. 값 타입(예: int에서 double로)은 안 돼.
중요: 객체가 항상 원하는 타입임을 확신하거나, 값 타입 변환이 필요하면 직접 변환 (타입)표현식을 써. 변환 불가 시 직접 변환은 에러(InvalidCastException)를 던지니까, 타입이 잘못된 게 진짜 버그일 때는 이게 더 좋아.
GO TO FULL VERSION