객체지향 프로그래밍(Object-Oriented Programming, OOP): 현실 세계의 객체를 프로그램에 반영하여, 소프트웨어를 구조적으로 설계하고 구현하는 프로그래밍 패러다임.OOP에서는 프로그램을 구성하는 기본 단위를 객채로 보고, 이 객체들이 서로 상호작용하여 기능을 수행하도록 함. 객체는 속성과 기능으로 구성되며, 이 속성과 기능을 클래스로 정의한 후 이를 바탕으로 객체를 생성하게 됨.
속성(Attribute): 객체가 가지는 고유한 특성이나 상태. 변수로 정의되며, 객체의 상태를 저장하고 관리하는 역할. (데이터)
public class Car
{
// 속성 (필드)
public string Color;
public string Model;
public int FuelLevel;
}
기능(Functionality): 객체가 수행할 수 잇는 동작이나 행동. 메소드로 정의되며, 객체가 특정 동작을 실행할 때 사용됨.
public class Car
{
public string Color;
public string Model;
public int FuelLevel;
// 기능 (메서드)
public void Drive()
{
Console.WriteLine("자동차가 운전 중이다.");
FuelLevel -= 10; // 연료 감소
}
public void Refuel(int amount)
{
FuelLevel += amount;
Console.WriteLine("연료가 채워졌다.");
}
}
객체지향의 특징
- 캡슐화: 데이터(속성)와 기능을 하나의 단위로 묶어 외부에서 접근할 수 없도록 보호하는 개념. 데이터 무결성 유지.
- 상속: 기존 클래스의 속성과 기능을 그대로 물려받고, 필요한 부분만 추가하거나 수정하여 새로운 클래스를 생성하는 것.
- 다형성: 같은 이름의 메소드가 다양한 형태로 동작할 수 있게 만드는 것.
- 추상화: 복잡한 시스템을 간단하게 표현하는 방법, 핵심적인 속성과 기능만 노출하여 사용하기 쉽게 함.
클래스(Class): 데이터와 해당 데이터를 처리하는 메소드(함수)를 하나의 단위로 묶어 관리. 객체 지향 프로그래밍에서 객체를 생성하기 위한 청사진(설계도).
클래스의 구조
public class 클래스이름
{
// 필드(변수)
private int number;
// 생성자
public 클래스이름()
{
number = 0;
}
// 메서드
public void SetNumber(int value)
{
number = value;
}
public int GetNumber()
{
return number;
}
}
- 필드(변수): 클래스의 속성을 저장하기 위한 변수, 객체마다 고유의 값을 가짐.
- 생성자: 객체가 생성될 때 호출되는 특별한 메소드. 필드를 초기화하거나 기본값을 설정하는데 사용됨.
- 메소드: 객체의 행동을 정의, 클래스 외부에서 호출할 수 있음.
생성자(Constructor): 객체가 생성될 때 자동으로 호출되는 메소드, 클래스의 이름과 동일한 이름을 가지며 반환형이 없음. 주로 객체의 초기화 작업을 수행하는데 사용됨.
특징
- 클래스의 이름과 동일한 이름을 가짐.
- 반환형이 없음.
- 객체가 생성될 때 한번만 호출됨.
- 오버로딩이 가능하며, 매개변수에 따라 여러 형태의 생성자 정의 가능.
public class Car
{
public string Color;
public string Model;
public int FuelLevel;
// 생성자
public Car(string color, string model)
{
Color = color;
Model = model;
FuelLevel = 100; // 기본 연료량 설정
Console.WriteLine("자동차 객체가 생성되었습니다.");
}
}
종료자(Finalizar): 객체가 소멸될 때 호출되는 메소드로, 정리 작업을 수행함. 주로 메모리나 리소스를 해제하는데 사용되며, 클래스 이름 앞에 ~를 붙여 정의. 종료자는 명시적으로 호출하지 않으며, 가비지컬렉터가 객체를 메모리에서 제거할 때 자동으로 호출됨.
특징
- 클래스의 이름 앞에 ~를 붙여 정의함.
- 매개변수가 없으며, 오버로딩할 수 없음.
- 객체가 소멸될 때 호출되며, 직접 호출하지 않고 가비지 컬렉터에 의해 자동으로 호출됨.
public class Car
{
public string Color;
public string Model;
public int FuelLevel;
// 생성자
public Car(string color, string model)
{
Color = color;
Model = model;
FuelLevel = 100;
}
// 종료자
~Car()
{
Console.WriteLine("자동차 객체가 소멸되었습니다.");
}
}
this 키워드: 현재 객체 자신을 참조할 때 사용하는 특별한 키워드. 클래스 내부에서 해당 클래스로 생성된 현재 인스턴스를 가리키며, 주로 생성자, 메소드, 속성 등에서 사용됨.
주요 용도
- 인스턴스 변수와 매개변수 구분: 클래스의 필드(속성)와 메소드 또는 생성자의 매개변수 이름이 같을 때 this를 사용하여 인스턴스 변수를 명확히 구분할 수 있음.
public class Car
{
public string Color;
public Car(string Color)
{
// this를 사용하여 인스턴스 변수와 매개변수를 구분
this.Color = Color;
}
}
-this.color는 클래스의 인스턴스 변수, color는 매개변수
- 메서드 체이닝(Method Chaining): this 키워드는 메소드를 호출할 때 반환값으로 this를 반환하여 메서드 체이닝을 가능하게 함.
public class Car
{
public string Color;
public int FuelLevel;
public Car SetColor(string color)
{
this.Color = color;
return this;
}
public Car Refuel(int amount)
{
this.FuelLevel += amount;
return this;
}
}
메서드 체이닝을 사용하면 다음과 같이 메소드를 연속으로 호출할 수 있음.
Car car = new Car();
car.SetColor("Red").Refuel(50);
- 생성자에서 다른 생성자 호출: 같은 클래스 내에서 하나의 생성자가 다른 생성자를 호출할 때 사용 가능. 이를 통해 생성자 중복을 줄이고, 여러 초기화 로직을 통합할 수 있음.
public class Car
{
public string Color;
public int FuelLevel;
// 기본 생성자
public Car() : this("Black", 100) { }
// 매개변수가 있는 생성자
public Car(string color, int fuelLevel)
{
this.Color = color;
this.FuelLevel = fuelLevel;
}
}
- 이벤트 핸들러나 대리자(Delegate)에 현재 인스턴스를 전달
접근 한정자(Access Modifiers): 클래스, 메소드, 속성, 필드 등이 다른 클래스나 코드에서 접근할 수 있는 범위를 지정하는 키워드.
1.public: 모든 코드에서 접근 가능.주로 외부에서 사용해야 하는 메소드나 속성, 클래스 등을 정의할 때 사용.
public class Car
{
public string Color;
public void Drive()
{
Console.WriteLine("운전 중입니다.");
}
}
2.private: 클래스 내부에서만 접근 가능. 외부에서는 접근 할 수 없음. 기본 접근 한정자로, 명시하지 않으면 private으로 설정. 클래스 내부에서만 사용하는 필드나 메소드에서 사용하며 데이터를 보호함.
public class Car
{
private string engineType;
public void SetEngineType(string type)
{
engineType = type;
}
}
3.protected: 클래스 내부와 파생 클래스(자식 클래스)에서만 접근 가능함.
public class Car
{
protected string brand;
}
public class SportsCar : Car
{
public void ShowBrand()
{
Console.WriteLine("브랜드: " + brand);
}
}
4.internal: 같은 어셈블리(프로젝트) 내에서만 접근할 수 있음. 어셈블리를 벗어난 외부에서는 접근할 수 없음. 같은 프로젝트 내에서만 사용되는 클래스나 멤버에 사용됨.
internal class Car
{
internal string Model;
}
5.protected internal: 같은 어셈블리 내의 모든 코드와 파생 클래스에서 접근 가능. 같은 프로젝트 내에서나 상속받은 클래스에서는 접근 가능, 외부 프로젝트에서는 접근 불가능. 상속 관계가 있고, 어셈블리 내에서 널리 사용되지만, 외부에서는 제한하고 싶을 때 사용.
public class Car
{
protected internal string Model;
}
6.private protected: 같은 어셈블리 내의 파생 클래스에서만 접근 가능. 외부 어셈블리에서는 접근 불가능, 같은 어셈블리 내에서도 상속받은 클래스만 접근할 수 있음.
public class Car
{
private protected string model;
}
public class SportsCar : Car
{
public void ShowModel()
{
Console.WriteLine("모델: " + model);
}
}
파생 클래스: 기존 클래스의 특성과 동작을 물려받아 새로 정의된 클래스. 상속을 통해 기존 클래스의 속성(필드)과 기능(메소드)을 재사용하면서, 필요한 부분을 추가하거나 변경하여 자신만의 특성을 가진 클래스로 확장함.
파생 클래스 정의: : 기호를 사용하여 기본 클래스를 지정.
public class 기본클래스명 { ... }
public class 파생클래스명 : 기본클래스명 { ... }
// 기본 클래스
public class Car
{
public string Brand;
public int Speed;
public void Drive()
{
Console.WriteLine("차량이 주행 중입니다.");
}
}
// 파생 클래스
public class SportsCar : Car
{
public int TurboSpeed;
public void ActivateTurbo()
{
Console.WriteLine("터보 모드가 활성화되었습니다!");
Speed += TurboSpeed;
}
}
특징
- 기본 클래스의 멤버를 상속: 파생 클래스는 기본 클래스의 필드, 속성, 메소드를 자동으로 상속받아 사용할 수 있음.
- 기능 확장: 파생 클래스는 기본 클래스에 없는 새로운 멤버(속성이나 메소드)를 추가할 수 있음.
- 오버라이딩(overriding): 파생 클래스에서 기본 클래스의 메소드를 재정의 할 수 있음. 이때 virtual 과 override 키워드를 사용하여 기본 클래스의 메소드를 파생 클래스에서 새롭게 구현할 수 있음.
//메소드 오버라이딩
public class Car
{
public virtual void Drive()
{
Console.WriteLine("차량이 주행 중입니다.");
}
}
public class SportsCar : Car
{
public override void Drive()
{
Console.WriteLine("스포츠카가 고속으로 주행 중입니다!");
}
}
상속의 장점과 제약 사항
- 코드 재사용: 기본 클래스의 멤버를 재사용함으로써 중복 코드를 줄이고 유지보수성을 높일 수 있음.
- 확장성: 기본 클래스의 기능을 파생 클래스에서 확장하여 새로운 기능을 추가하거나 기존 기능을 변경할 수 있음.
- 유연성: 다형성을 통해 기본 클래스와 파생 클래스를 같은 타입으로 다룰 수 있어 객체 간의 상호작용을 유연하게 설계할 수 있음.
- C#에서는 다중 상속(하나의 클래스가 여러 기본 클래스를 상속받는 것)을 지원하지 않으며, 한 클래스는 오직 하나의 기본 클래스만 상속받을 수 있음.
- sealed 키워드를 사용하여 특정 클래스의 상속 금지 가능.
base 키워드: 파생 클래스에서 기본 클래스의 멤버(필드, 메소드, 생성자 등)에 접근할 때 사용되는 키워드. 기본 클래스의 생성자를 호출하거나 재정의된 메소드를 호출할 때 사용.
주요 용도
- 기본 클래스의 생성자 호출: 파생 클래스의 생성자에서 base를 사용하여 기본 클래스의 생성자 호출 가능.
- 기본 클래스의 메소드나 속성 접근: 파생 클래스에서 기본 클래스의 메소드를 재정의(오버라이딩)한 경우, base를 사용하여 재정의된 기본 클래스의 메소드나 속성 호출 가능.
//기본 클래스 생성자 호출
public class Car
{
public string Brand;
public int Year;
// 기본 클래스 생성자
public Car(string brand, int year)
{
Brand = brand;
Year = year;
}
}
public class SportsCar : Car
{
public int MaxSpeed;
// 파생 클래스 생성자에서 기본 클래스 생성자 호출
public SportsCar(string brand, int year, int maxSpeed) : base(brand, year)
{
MaxSpeed = maxSpeed;
}
}
//기본 클래스 메소드 호출
public class Car
{
public virtual void Drive()
{
Console.WriteLine("차량이 주행 중입니다.");
}
}
public class SportsCar : Car
{
public override void Drive()
{
base.Drive(); // 기본 클래스의 Drive 메서드 호출
Console.WriteLine("스포츠카가 고속으로 주행 중입니다!");
}
}
형식 변환: 특정 데이터 형식을 다른 형식으로 변환하는 것. 이 과정에서 is와 as 키워드가 자주 사용됨. 이러한 키워드는 객체의 타입을 확인하고, 안전하게 형변환을 수행하는 유용함.
- 암시적 변환(Implicit Conversion): 데이터 손실이 없고 안전한 경우, 자동으로 수행됨.
int num = 10;
double result = num; // int를 double로 암시적 변환
- 명시적 변환(Explicit Conversion): 데이터 손실이 발생할 수 있거나 타입이 달라서 명시적으로 변환해야 할 경우, cast를 사용하여 변환.
double num = 10.5;
int result = (int)num; // double을 int로 명시적 변환 (소수점 손실 발생)
is 키워드: 객체가 특정 타입에 해당하는지 확인할 때 사용. is는 참 또는 거짓의 불리언 값을 반환함. 주로 조건문에서 객체의 타입을 확인할 때 유용함.
object obj = "Hello";
if (obj is string)
{
Console.WriteLine("obj는 문자열 타입입니다.");
}
else
{
Console.WriteLine("obj는 문자열 타입이 아닙니다.");
}
as 키워드: 형변환을 시도하고, 실패하면 null을 반환. as는 주로 참조 형식 간의 형변환에 사용됨. 형 변환에 실패할 경우 예외를 던지지 않고 null을 반환하므로, 보다 안전하게 형 변환을 처리할 수 있음.
object obj = "Hello";
string str = obj as string;
if (str != null)
{
Console.WriteLine("형변환에 성공하여 str에 값이 저장되었습니다: " + str);
}
else
{
Console.WriteLine("형변환에 실패하여 str은 null입니다.");
}
//is와 as를 활용한 타입 검사 및 형변환 예시
object obj = "Hello";
if (obj is string str) // C# 패턴 매칭을 사용하여 동시에 형변환
{
Console.WriteLine("형변환 성공: " + str);
}
else
{
Console.WriteLine("형변환 실패");
}
오버라이딩(overriding): 부모 클래스에서 정의한 메소드를 파생 클래스에서 재정의(덮어쓰기)하는 것. 오버라이딩을 통해 파생 클래스는 부모 클래스의 기본 동작을 변경하거나 확장 가능.
규칙
- 부모 클래스의 메소드는 virtual 키워드로 선언되어 있어야 함.
- 파생 클래스에서는 override 키워드를 사용하여 부모 메소드를 재정의함.
- 오버라이딩된 메소드는 부모 클래스의 메소드와 같은 시그니처(이름, 매개변수)를 가져야 함.
public class Animal
{
// 부모 클래스의 메서드를 virtual로 선언
public virtual void Speak()
{
Console.WriteLine("동물이 소리를 냅니다.");
}
}
public class Dog : Animal
{
// 파생 클래스에서 부모 메서드를 override하여 재정의
public override void Speak()
{
Console.WriteLine("강아지가 멍멍 짖습니다.");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("고양이가 야옹 울습니다.");
}
}
-Animal 클래스의 Speak 메소드는 virtual로 선언되어 있으며, Dog와 Cat 클래스는 각각 이 메소드를 override하여 재정의 함. 따라서 Dog와 Cat 객체는 Speak 메소드를 호출할 때 각자의 고유한 동작을 수행함.
Animal myDog = new Dog();
myDog.Speak(); // 출력: 강아지가 멍멍 짖습니다.
Animal myCat = new Cat();
myCat.Speak(); // 출력: 고양이가 야옹 울습니다.
다형성(Polymorphism): 같은 타입의 객체가 서로 다른 동작을 수행할 수 있는 능력을 의미. 하나의 인터페이스로 여러 객체를 다룰 수 있고, 객체의 실제 타입에 따라 적절한 동작을 수행할 수 있음. 다형성은 상속과 오버라이딩을 통해 구현됨.
- 런타입 다형성: 오버라이딩을 통해 구현됨, 메소드는 실제로 실행될 때 객체의 타입에 따라 적절한 메소드가 호출됨.
- 컴파일타임 다형성: 메소드 오버로딩과 같은 방식으로, 메소드가 컴파일될 때 호출할 메소드가 결정됨.
List<Animal> animals = new List<Animal>();
animals.Add(new Dog());
animals.Add(new Cat());
foreach (Animal animal in animals)
{
animal.Speak(); // 각 객체의 타입에 맞는 Speak 메서드가 호출됨
}
메소드 숨기기(Method Hiding): 부모 클래스의 메소드를 파생 클래스에서 동일한 이름으로 새로 정의하는 것. 메소드 숨기기를 통해 파생 클래스에서 부모 클래스의 메소드를 감추고, 새로운 동작을 부여할 수 있음.
메소드 숨기기는 주로 new 한정자를 사용하여 명시적으로 선언함(≠new 연산자). override와 달린 메소드 숨기기는 오버라이딩이 아닌 메소드 재정의 방식으로 이루어지며, 부모 클래스의 메소드와 동일한 시그니처(메소드 이름과 매개변수)를 가지는 경우에 사용됨.
특징
- new 키워드를 사용하여 메소드를 숨김.
- 부모 클래스와 같은 이름의 메소드를 파생 클래스에서 새로 정의하여, 다른 동작을 수행하도록 할 수 있음.
- 부모 클래스의 메소드를 덮어쓰지만, 런타임 다형성이 적용되지 않음. 즉, 부모 클래스의 참조 변수를 사용할 때는 부모 클래스의 메소드가 호출됨.
public class Parent
{
public void ShowMessage()
{
Console.WriteLine("부모 클래스의 메서드입니다.");
}
}
public class Child : Parent
{
// 메서드 숨기기: new 키워드를 사용하여 부모 메서드를 숨김
public new void ShowMessage()
{
Console.WriteLine("자식 클래스에서 숨긴 메서드입니다.");
}
}
-Parent 클래스의 ShowMessage 메소드는 Child 클래스에서 동일한 이름으로 재정의 됨. new 키워드를 사용하여 부모 클래스의 메소드를 숨기고 새로운 동작을 정의함.
메소드 숨기기의 동작 예시
Parent parent = new Parent();
parent.ShowMessage(); // 출력: "부모 클래스의 메서드입니다."
Child child = new Child();
child.ShowMessage(); // 출력: "자식 클래스에서 숨긴 메서드입니다."
Parent polymorphicChild = new Child();
polymorphicChild.ShowMessage(); // 출력: "부모 클래스의 메서드입니다."
- parent.ShowMessage()는 부모 클래스의 메소드를 호출.
- child.ShowMessage()는 파생 클래스의 ShowMessage 메소드를 호출.
- polymorphicChild.ShowMessage()는 부모 클래스의 참조 타입으로 child 객체를 호출했지만, 메소드 숨기기에서는 다형성이 적용되지 않아 부모 클래스의 ShowMessage 메소드 호출.
오버라이딩과의 차이점
- 오버라이딩은 virtual과 override를 사용하여 런타입 다형성을 구현하며, 부모 클래스의 메소드를 재정의 하는 방식.
- 메소드 숨기기는 new 키워드를 사용하여 부모 클래스의 메소드를 감추고 새로운 메소드를 정의하는 방식으로, 런타임 다형성이 적용되지 않음.
읽기 전용 필드(Read-Only Field): C#에서 한번 초기화된 후 값을 변경할 수 없는 필드. 읽기 전용 필드는 주로 readonly 키워드를 사용해 정의되며, 값을 수정할 수 없는 상수 역할을 함.
특징
- readonly 키워드를 사용하여 선언.
- 생성자에서 초기화 할 수 있음. (특히 객체 생성 시 동적으로 값을 설정해야 하는 경우 유용함.)
- 한 번 설정된 값은 이후 변경할 수 없으며, 오직 생성자에서만 값을 할당할 수 있음.
- const와 달리 컴파일 시점에 값이 확정될 필요가 없음. 실행 중 특정 조건에 따라 값을 설정할 수 있음.
읽기 전용 필드와 상수의 차이점
- const는 컴파일 시간 상수로, 컴파일 시점에 고정된 값을 가져야 하며, 숫자, 문자열 등 단순 데이터 타입에만 가능.
- readonly 필드는 런타임 상수로, 생성자에서 값을 설정할 수 있고, 실행 중에만 알 수 있는 값도 할당 가능.
public class Car
{
// 읽기 전용 필드 선언
public readonly string Brand;
public readonly int Year;
// 생성자를 통해 읽기 전용 필드 초기화
public Car(string brand, int year)
{
Brand = brand;
Year = year;
}
public void DisplayInfo()
{
Console.WriteLine($"브랜드: {Brand}, 연식: {Year}");
}
}
//사용예시
Car car = new Car("Hyundai", 2023);
car.DisplayInfo(); // 출력: "브랜드: Hyundai, 연식: 2023"
// car.Brand = "Kia"; // 오류 발생: 읽기 전용 필드이므로 값을 변경할 수 없음
읽기 전용 필드를 사용하는 이유
- 객체가 생성된 이후 절대 변경되어서는 안되는 값을 보호할 수 있음.
- 생성자에서 초기화할 수 있어, 객체마다 고유한 값을 설정할 수 있음.
- 코드의 안전성을 높이고, 예기치 않은 값 변경을 방지할 수 있음.
중첩 클래스(Nested Class): 하나의 클래스 내부에 정의된 클래스. 외부 클래스와 긴밀한 관계를 가지며, 주로 외부 클래스의 작업을 지원하는 보조적인 역할을 함.
특징
- 외부 클래스와의 관계: 중첩 클래스는 외부 클래스의 멤버에 직접 접근 가능. (접근 제한자가 허용하는 범위 내에서)
- 캡슐화: 외부 클래스에서만 사용되는 특정 기능을 중첩 클래스로 정의하여, 외부 코드에서 접근할 수 없게 만들어 코드의 캡슐화를 강화할 수 있음.
- 조직화: 외부 클래스와 밀접한 관계가 있는 클래스를 내부에 정의하여 클래스 구조를 더 체계적으로 관리할 수 있음.
public class OuterClass
{
private string message = "외부 클래스의 메시지";
// 중첩 클래스 정의
public class InnerClass
{
public void Display()
{
Console.WriteLine("중첩 클래스의 Display 메서드 호출");
}
}
public void ShowMessage()
{
InnerClass inner = new InnerClass();
inner.Display();
Console.WriteLine(message);
}
}
-InnerClass는 OuterClass 내부에 정의된 중첩 클래스. OuterClass의 ShowMessage 메소드는 InnerClass의 인스턴스를 생성하고, Display 메소드를 호출할 수 있음.
OuterClass outer = new OuterClass();
outer.ShowMessage();
// 출력:
// 중첩 클래스의 Display 메서드 호출
// 외부 클래스의 메시지
중첩 클래스의 접근 제한: 일반클래스와 동일하게 접근 제한자를 사용할 수 있음. 접근 제한에 따라 외부 클래스 또는 외부 코드에서 접근 가능 여부가 달라짐.
장점
- 캡슐화 강화: 외부 클래스에서만 사용되는 클래스를 내부에 정의하여 코드의 접근성을 제한하고, 코드의 응집도를 높임.
- 논리적 그룹화: 외부 클래스와 밀접하게 관련된 기능을 내부에 정의함으로써 코드의 가독성을 높이고, 클래스를 더 체계적으로 관리할 수 있음.
- 외부 클래스와의 접근성: 중첩 클래스는 외부 클래스의 private 멤버에도 접근할 수 있어, 관련 작업을 쉽게 수행할 수 있음.
분할 클래스(Partial Class): C#에서 하나의 클래스를 여러 파일에 나누어 정의할 수 있도록 해주는 기능.
특징
- partial 키워드: 클래스 선언 시 partial 키워드를 사용하여 해당 클래스가 분할될 수 있음을 명시.
- 하나의 클래스처럼 동작: 분할된 파일에 정의된 멤버들은 컴파일 시 하나의 클래스로 결합되므로, 마치 하나의 클래스처럼 동작함.
- 코드 관리 용이성: 큰 클래스를 여러 파일로 나누어 유지보수가 쉽고, 여러 개발자가 동시에 작업할 때 유리함.
// Person1.cs 파일
public partial class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void DisplayFullName()
{
Console.WriteLine($"{FirstName} {LastName}");
}
}
// Person2.cs 파일
public partial class Person
{
public int Age { get; set; }
public void DisplayInfo()
{
Console.WriteLine($"Name: {FirstName} {LastName}, Age: {Age}");
}
}
-Person 클래스는 두 개의 파일에 나누어 정의됨. 각 파일에서 partial 키워드를 사용하여 동일한 클래스를 나누어 구현함. 두 파일은 컴파일 시 하나의 Person 클래스로 결합됨.
Person person = new Person();
person.FirstName = "John";
person.LastName = "Doe";
person.Age = 30;
person.DisplayFullName(); // 출력: John Doe
person.DisplayInfo(); // 출력: Name: John Doe, Age: 30
-Person 클래스는 Person1.cs와 Person2.cs 파일에 나누어 정의되었지만, 컴파일후 하나의 클래스로 결합되어 모든 멤버에 접근할 수 있음.
장점
- 코드 관리의 편리성: 클래스가 복잡하고 길어질 때, 기능별로 분리하여 코드를 유지보수하기 쉬움.
- 여러 개발자의 협업 용이: 하나의 클래스를 여러 파일로 나눠 작업할 수 있어, 여러 개발자가 동시에 작업할 때 충돌을 줄일 수 있음.
- 자동 생성 코드와 사용자 정의 코드의 분리: 코드 생성 도구가 자동 생성하는 코드와 사용자 정의 코드를 별도 파일로 분리하여, 코드 생성 도구가 파일을 덮어쓸 때 사용자 정의 코드가 영향을 받지 않도록 할 수 있음.
주의 사항
- 모든 partial 클래스는 같은 접근 수준을 가져야 함.
- 같은 네임스페이스 안에 있어야 하며, 같은 어셈블리에서 컴파일되어야 함.
- 정적 클래스(static class)도 partial로 나누어 정의할 수 있음.
확장 메소드(Extension Method): 기존 클래스에 새로운 메소드를 추가하는 방법으로, 원래의 클래스 코드나 상속을 변경하지 않고도 기능을 확장할 수 있는 기능. 주로 LINQ나 컬렉션, 문자열 처리 등에 유용하게 사용되며, static 클래스와 this 키워드를 사용하여 정의됨.
특징
- static 메소드로 정의됨.
- 메소드를 정의할 때 첫 번째 매개변수에 this 키워드를 사용하여 확장하고자 하는 클래스 타입을 지정함.
- 확장 메소드는 원래 클래스의 인스턴스 메소드처럼 호출할 수 있음.
- 내정된 클래스뿐만 아니라, 사용자 정의 클래스에도 확장 메소드를 추가할 수 있음.
using System;
public static class StringExtensions
{
// 확장 메소드 정의
public static int WordCount(this string str)
{
if (string.IsNullOrEmpty(str))
return 0;
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
- StringExternsions 라는 static 클래스에 WordCount 메소드가 정의됨. thisstring str 매개변수를 통해 string 타입을 확장하고 있음.
class Program
{
static void Main()
{
string text = "Hello, how are you?";
// WordCount 확장 메소드 호출
int count = text.WordCount();
Console.WriteLine($"단어 개수: {count}");
}
}
장점
- 기존 클래스 수정 없이 기능 확장: 기존 클래스를 변경하지 않고도 메소드를 추가할 수 있어, 외부 라이브러리나 기본 클래스의 기능을 확장할 때 유용함.
- 직관적인 사용: 확장 메소드를 통해 기존 인스턴스 메소드처럼 메소드를 호출할 수 있어, 코드가 더 직관적이고 읽기 쉬움.
- 코드 유지보수성 향상: 자주 사용하는 기능을 확장 메소드로 정의하면, 중복 코드를 줄이고 유지보수가 용이해짐.
주의 사항
- 확장 메소드는 static 클래스 안에 정의 되어야 함.
- 확장 메소드의 우선순위는 인스턴스 메소드보다 낮음. 동일한 이름의 인스턴스 메소드가 있으면, 확장 메소드가 아닌 인스턴스 메소드가 호출됨.
- 남용할 경우 코드가 복잡해질 수 있으므로, 실제 필요한 경우에만 사용해야 함.
구조체(Struct): C#에서 값 타입(Value Type)을 정의하기 위한 데이터 구조. 클래스와 유사하게 필드, 메소드, 프로퍼티, 생성자 등을 가질 수 있지만, 주로 작고 단순한 데이터를 표현할 때 사용함.
특징
- 값 타입: 구조체는 값 타입으로, 힙이 아닌 스택에 저장되며 객체를 복사할 때 값이 복사됨. (클래스는 참조 타입)
- 상속 불가: 구조체는 클래스를 상속하거나 상속받을 수 없음. 단, 인터페이스는 구현할 수 있음.
- 기본 생성자 없음: 구조체는 매개변수가 없는 생성자를 정의할 수 없음. 컴파일러가 자동으로 기본 생성자를 제공하며, 모든 필드는 기본값으로 초기화됨.
- 불변성(Immutable): 구조체는 주로 변경 불가능하게(immutable) 설계하여 사용하는 경우가 많음. 불변 구조체는 객체가 생성된 이후 필드 값을 변경할 수 없도록 설계되어, 특히 멀티스레드 환경에서 안전하게 사용 가능.
public struct Point
{
public int X { get; }
public int Y { get; }
// 생성자를 통해 값 초기화
public Point(int x, int y)
{
X = x;
Y = y;
}
// 메서드 정의
public double DistanceTo(Point other)
{
int dx = X - other.X;
int dy = Y - other.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
-Point 구조체는 X와 Y 좌표 값을 저장하며, 거리 계산 메소드 DistanceTo를 정의함. 생성자를 통해 초기화되며, X와 Y는 읽기 전용 프로퍼티로 설정되어 불변성을 유지.
Point p1 = new Point(3, 4);
Point p2 = new Point(0, 0);
Console.WriteLine($"p1의 좌표: ({p1.X}, {p1.Y})");
Console.WriteLine($"p2까지의 거리: {p1.DistanceTo(p2)}");
변경 불가능한(Immutable) 구조체: 구조체는 불변하게 설계하는 것이 권장됨. 구조체의 필드가 초기화된 후 변경되지 않도록 설계하는 것이며, 특히 멀티스레드 환경에서 안전하게 사용할 수 있음. 불변 구조체는 모든 필드를 읽기 전용으로 설정하여, 구조체가 생성된 이후에 필드 값이 변경되지 않도록 함.
public struct ImmutablePoint
{
public int X { get; }
public int Y { get; }
// 생성자를 통해 초기화하며, 이후 값 변경 불가능
public ImmutablePoint(int x, int y)
{
X = x;
Y = y;
}
// 새로운 값을 가진 새로운 인스턴스를 반환하는 메서드
public ImmutablePoint Move(int deltaX, int deltaY)
{
return new ImmutablePoint(X + deltaX, Y + deltaY);
}
}
-ImmutablePoint 구조체는 불변 구조체로, X와 Y 필드는 읽기 전용 프로퍼티로 설정되었으며, 초기화 이후 변경할 수 없음. Move 메소드는 좌표 이동 기능을 제공하지만, 원래의 ImmutablePoint 인스턴스를 변경하지 않고 새로운 값을 가진 인스턴스를 반환함.
ImmutablePoint p1 = new ImmutablePoint(5, 10);
ImmutablePoint p2 = p1.Move(3, 4);
Console.WriteLine($"p1: ({p1.X}, {p1.Y})"); // p1: (5, 10)
Console.WriteLine($"p2: ({p2.X}, {p2.Y})"); // p2: (8, 14)
구조체와 클래스의 차이점
특성 |
구조체(Struct) |
클래스(Class) |
타입 |
값 타입(Value Type) |
참조 타입(Reference Type) |
메모리 할당 위치 |
스택(Stack) |
힙(Heap) |
상속 |
상속 불가, 인터페이스 구현만 가능 |
상속 가능 |
기본 생성자 |
사용자 정의 기본 생성자 없음 |
기본 생서자 가능 |
불변성 |
불변 구조체로 설계 권장 |
필요에 따라 설정 가능 |
튜플(Tuple): 여러 개의 값을 하나의 그룹으로 묶어 저장할 수 있는 데이터 구조. 반환값이 하나뿐인 메소드에서도 여러 개의 값을 반환할 수 있으며, 여러 데이터 항목을 간단하게 저장하고 사용할 수 있음.
특징
- 다수의 값을 한 번에 저장: 여러 타입의 값을 하나로 묶어 저장 가능.
- 익명 필드: 기본적으로 필드 이름 없이 인덱스로 접근하거나, 필요한 경우 필드 이름을 지정할 수 있음.
- 읽기 전용: C#의 튜플은 불변. 생성된 후 튜플 항목의 값을 변경할 수 없음.
- 간단한 사용: 데이터를 빠르게 묶어서 전달하거나 반환할 때 편리하게 사용할 수 있음.
var person = ("Alice", 30); // 이름과 나이를 저장한 튜플
Console.WriteLine($"이름: {person.Item1}, 나이: {person.Item2}");
//필드 이름을 지정한 튜플
var person = (Name: "Alice", Age: 30);
Console.WriteLine($"이름: {person.Name}, 나이: {person.Age}");
튜플을 활용한 여러 값 반환: 메소드에서 여러 값을 반환할 때 유용하게 사용할 수 있음.
public static (int Sum, int Product) Calculate(int a, int b)
{
return (a + b, a * b);
}
var result = Calculate(3, 4);
Console.WriteLine($"합계: {result.Sum}, 곱셈: {result.Product}");
장점과 단점
- 간단한 데이터 묶음 처리: 특정 클래스를 만들지 않고도 여러 데이터를 묶어 관리할 수 있음.
- 메소드에서 여러 값 반환: 메소드가 여러 값을 반환해야 할 때 튜플을 사용하면 깔끔하게 구현할 수 있음.
- 코드 가독성 향상: 필드 이름을 지정하면 코드의 가독성이 높아짐.
- 데이터의 의미가 명확하지 않음: 클래스나 구조체에 비해 데이터의 의미를 덜 명확할 수 있음.
- 성능: 빈번하게 큰 데이터 묶음을 처리할 경우 클래스나 구조체가 더 적합할 수 있음.
위치 패턴 매칭(Positional Pattern Matching): 튜플이나 특정 클래스의 개별 속성 값을 패턴으로 매칭하여 조건을 검사하거나 값을 분해할 수 있게 해주는 기능. swich 구문에서 많이 사용되며, 튜플과 레코드 같은 데이터 구조를 조건에 따라 쉽게 분해하고 검사할 수 있도록 함.
특징
- 데이터 구조의 개별 요소 매칭: 객체의 내부 값들을 쉽게 패턴으로 정의하여 분해하고, 그 값을 기반으로 조건을 검사할 수 있음.
- 간결한 조건 검사: 복잡한 조건 검사를 간결하게 작성할 수 있음.
- 주로 튜플과 레코드에서 사용: 각 요소를 확인하고 분해하는데 유용함.
//튜플을 이용한 위치 패턴 매칭
(string, int) person = ("Alice", 30);
string result = person switch
{
("Alice", 30) => "Alice is 30 years old.",
("Alice", _) => "Alice's age is unknown.",
(_, 30) => "Someone is 30 years old.",
_ => "Unknown person"
};
Console.WriteLine(result); // 출력: Alice is 30 years old.
//레코드를 이용한 위치 패턴 매칭
public record Point(int X, int Y);
Point point = new Point(3, 4);
string position = point switch
{
(0, 0) => "The point is at the origin.",
(3, 4) => "The point is at (3, 4).",
(_, 4) => "The point has a Y coordinate of 4.",
_ => "The point is somewhere else."
};
Console.WriteLine(position); // 출력: The point is at (3, 4).
장점
- 코드 가독성 향상: 복잡한 조건 검사를 패턴을 이용해 간결하고 직관적으로 표현할 수 있음.
- 구조화된 데이터 검사에 용이: 튜플과 레코드처럼 구조화된 데이터의 개별 요소를 쉽게 검사할 수 있음.
- 유연한 조건 검사: 와일드 카드_와 함께 사용하여 특정 위치만 검사하거나, 특정 요소는 무시하는 등 유연하게 조건을 설정할 수 있음.