C# 문법 종합반 3주차 강의 정리
객체지향 프로그래밍(Object-Oriented Programming, OOP)의 특징
캡슐화 (Encapsulation):
관련된 데이터와 기능을 하나의 단위로 묶는 것을 의미합니다.
클래스를 사용하여 데이터와 해당 데이터를 조작하는 메서드를 함께 캡슐화하여 정보를 은닉하고, 외부에서 직접적인 접근을 제한함으로써 안정성과 유지보수성을 높입니다.
상속 (Inheritance):
상속은 기존의 클래스를 확장하여 새로운 클래스를 만드는 메커니즘입니다.
부모 클래스(상위 클래스, 슈퍼 클래스)의 특성과 동작을 자식 클래스(하위 클래스, 서브 클래스)가 상속받아 재사용할 수 있습니다.
코드의 중복을 줄이고, 클래스 간 계층 구조를 구성하여 코드의 구조화와 유지보수를 용이하게 합니다.
다형성 (Polymorphism):
다형성은 하나의 인터페이스나 기능을 다양한 방식으로 구현하거나 사용할 수 있는 능력을 의미합니다.
하나의 메서드 이름이 다양한 객체에서 다르게 동작할 수 있도록 하는 것으로, 오버로딩과 오버라이딩을 통해 구현됩니다.
유연하고 확장 가능한 코드 작성을 가능하게 하며, 코드의 가독성과 재사용성을 높입니다.
추상화 (Abstraction):
추상화는 복잡한 시스템이나 개념을 단순화하여 필요한 기능에 집중하는 것을 의미합니다.
클래스나 인터페이스를 사용하여 실제 세계의 개념을 모델링하고, 필요한 부분에 대한 명세를 정의합니다.
세부 구현 내용을 감추고 핵심 개념에 집중함으로써 코드의 이해와 유지보수를 용이하게 합니다.
객체 (Object):
객체는 클래스로부터 생성된 실체로, 데이터와 해당 데이터를 조작하는 메서드를 가지고 있습니다.
객체는 상태(데이터)와 행동(메서드)을 가지며, 실제 세계의 개체나 개념을 모델링합니다.
객체들 간의 상호작용을 통해 프로그램이 동작하고, 모듈화와 재사용성을 높입니다.
생성자, 소멸자
생성자
기본적으로 매개변수가 없는 디폴트 생성자가 클래스에 자동으로 생성되지만, 사용자가 직접 정의한 생성자가 있는 경우 디폴트 생성자가 자동으로 생성되지 않습니다. ( 디폴트 생성자가 필요하면 직접 만들어야함 )
소멸자
C#에서는 가비지 컬렉터(Garbage Collector)에 의해 관리되는 메모리 해제를 담당하므로, 명시적으로 소멸자를 호출하는 것은 일반적으로 권장되지 않습니다.
오버로딩
- 메서드 오버로딩은 동일한 이름의 메서드를 다양한 매개변수 목록으로 다중 정의하는 개념입니다.
- 매개변수의 개수, 타입, 순서가 다른 여러 메서드를 동일한 이름으로 정의하여 메서드 호출 시 매개변수의 형태에 따라 적절한 메서드가 선택되도록 할 수 있습니다.
- 오버로딩은 메서드의 기능이나 작업은 동일하지만 입력값에 따라 다르게 동작해야 할 때 사용됩니다.
- 반환값은 의미가 없다.
void PrintMessage(string message)
{
Console.WriteLine("Message: " + message);
}
void PrintMessage(int number)
{
Console.WriteLine("Number: " + number);
}
// 메서드 호출
PrintMessage("Hello, World!"); // 문자열 매개변수를 가진 메서드 호출
PrintMessage(10); // 정수 매개변수를 가진 메서드 호출
아래의 예시를 보면 반환값이 다르고, 매개변수는 같습니다. 그런데 오류가 발생합니다. 이는 '반환값은 아무 의미가 없다'는 뜻입니다.
// 오류 발생
long PrintMessage(int number)
{
Console.WriteLine("Message: " + message);
}
int PrintMessage(int number)
{
Console.WriteLine("Number: " + number);
}
프로퍼티
프로퍼티란?
`private`로 선언된 `필드(변수)`들을 외부에서도 접근할 수 있는 중간 매개역할로 만들어주는 것
직접 접근은 할 수 없지만 간접 접근이 가능하도록 하는 것
필드에 대한 접근 제어와 데이터 유효성 검사 등을 추가적인 로직을 구현하여 수행할 수 있습니다.
프로퍼티 구문
[접근 제한자] [데이터 타입] 프로퍼티명
{
get
{
// 필드를 반환하거나 다른 로직 수행
}
set
{
// 필드에 값을 설정하거나 다른 로직 수행
}
}
프로퍼티 사용 예시
class Person
{
private string name;
private int age;
public string Name
{
get { return name; }
set { name = value; }
}
public int Age
{
get { return age; }
set { age = value; }
}
}
Person person = new Person();
person.Name = "John"; // Name 프로퍼티에 값 설정
person.Age = 25; // Age 프로퍼티에 값 설정
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); // Name과 Age 프로퍼티
프로퍼티 접근 제한자 적용 & 유효성 검사 예제
class Person
{
private string name;
private int age;
public string Name
{
get { return name; }
private set { name = value; } // 클래스 내에서만 set 할 수 있음
}
// 유효성 검사
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
}
}
}
Person person = new Person();
person.Name = "John"; // 컴파일 오류: Name 프로퍼티의 set 접근자는 private입니다.
person.Age = -10; // 유효성 검사에 의해 나이 값이 설정되지 않습니다.
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); // Name과 Age 프로퍼티에 접근하여 값을 출력합니다.
자동 프로퍼티 (Auto Property)
[접근 제한자] [데이터 타입] 프로퍼티명 { get; set; }
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
다형성
같은 타입이지만 다양한 동작을 수행할 수 있는 능력
가상(Virtual) 메서드
가상 메서드는 기본적으로 부모 클래스에서 정의되고 자식 클래스에서 재정의할 수 있는 메서드입니다.
가상 메서드는 virtual 키워드를 사용하여 선언되며, 자식 클래스에서 필요에 따라 재정의될 수 있습니다.
이를 통해 자식 클래스에서 부모 클래스의 메서드를 변경하거나 확장할 수 있습니다.
아래 예시로 더 자세히 설명적겠습니다.
public class Unit
{
public void Move()
{
Console.WriteLine("두발로 걷기");
}
public void Attack()
{
Console.WriteLine("Unit 공격");
}
}
public class Marine : Unit
{
}
public class Zergling : Unit
{
public void Move()
{
Console.WriteLine("네발로 걷기");
}
}
// 사용 예시
// #1 참조형태와 실형태가 같을때
Marine marine = new Marine();
marine.Move();
marine.Attack();
Zergling zergling = new Zergling();
zergling.Move();
zergling.Attack();
// #2 참조형태와 실형태가 다를때
List<Unit> list = new List<Unit>();
list.Add(new Marine());
list.Add(new Zergling());
foreach (Unit unit in list)
{
unit.Move();
}
#1은 좋은 방법은 아닙니다. 이 방법이 다형성을 유지하는 방법은 아닙니다.
#1 에서 `Marine, Zergling` 의 타입으로 각각 정의했습니다. 하지만 종류도 다양해지고, 수량도 많아지면 관리하기가 힘들어집니다.
그래서 #2 처럼 상속하면서도 다형성을 유지하는 방법을 사용하는 것이 좋습니다.
`Move()`를 불러올 때 #1에서는 `자식 클래스`의 `Move()`를 불러오고, #2에서는 `부모의 클래스(Unit)`의 `Move()`를 불러옵니다.
이때 나오는 것이 `가상(Virtual) 메서드`입니다.
public class Unit
{
public virtual void Move()
{
Console.WriteLine("두발로 걷기");
}
public void Attack()
{
Console.WriteLine("Unit 공격");
}
}
public class Zergling : Unit
{
public override void Move()
{
Console.WriteLine("네발로 걷기");
}
}
#2의 방법을 사용하면서 부모 클래스가 아닌 자식 클래스의 Move()를 불러내는 방법은 부모 클래스의 Move()에 `virtual` 키워드를 붙이고, 자식 클래스의 Move()에 `override` 키워드를 붙이는 것입니다.
#2에서 `Unit`은 `참조형의 형태`이고, `실형태(값 형식)`는 `Marine`과 `Zergling`입니다.
참조형, 실형태(값 형식)란?
참조형은 메모리에 객체를 저장하고 변수는 해당 객체를 가리키는 포인터를 저장합니다.
값 형식은 변수가 실제 값을 가지고 있어 메모리에 저장됩니다.
`virtual` 키워드가 붙어있다면 "실형태가 다를 수 있으니 실형태에 재정의가 되어있는지 확인해봐라"는 느낌으로 받아들이면 됩니다.
추상(Abstract) 클래스와 메서드
- 추상 클래스는 직접적으로 인스턴스를 생성할 수 없는 클래스(추상적이라는건 두루뭉실하다는 거니까)
- 주로 상속을 위한 베이스 클래스로 사용됩니다.
- 추상 클래스는 `abstract` 키워드를 사용하여 선언되며, 추상 메서드를 포함할 수 있습니다.
- 추상 메서드는 구현부가 없는 메서드로, `자식 클래스`에서 반드시 구현되어야 합니다.
// 추상 클래스
abstract class Shape
{
// 선언만하고 정의를 안함.
public abstract void Draw(); // 추상 메서드
}
static void Main(string[] args)
{
// 오류 : 추상 형식 또는 인터페이스 'Program.Shape'의 인스턴스를 만들 수 없습니다.
Shape shape = new Shape();
}
위와 같이 Shape()를 만들어주면 에러가 난다. 선언만하고 정의를 안했기 때문이다.
그래서 추상 클래스는 인스턴스화(객체화)를 할 수 없다.
// 추상 클래스
abstract class Shape
{
public abstract void Draw(); // 추상 메서드
}
// 오류 : 'Program.Circle'은(는) 상속된 추상 멤버 'Program.Shape.Draw()'을(를) 구현하지 않습니다.
class Circle : Shape
{
}
추상 클래스를 사용하려면 위와같이 상속을 해주어야한다.
그런데 오류가 난다. Draw()가 구현이 안되어 있다고 한다. → 강제성 부여
// 추상 클래스
abstract class Shape
{
public abstract void Draw(); // 추상 메서드
}
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle");
}
}
class Square : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a square");
}
}
class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a triangle");
}
}
List<Shape> list = new List<Shape>();
list.Add(new Circle());
list.Add(new Square());
list.Add(new Triangle());
foreach (Shape shape in list )
{
shape.Draw();
}
virtual VS abstract
`virtual`은 없으면 내꺼 써~
`abstract`는 이건 무조건 재정의해!
오버라이딩(Overriding)
- 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의하는 것을 의미합니다.
- 이는 상속 관계에 있는 클래스 간에 발생하며, 메서드의 이름, 매개변수 및 반환타입이 동일해야 합니다.
- 오버라이딩을 통해 자식 클래스는 부모 클래스의 메서드를 재정의하여 자신에게 맞는 동작을 구현할 수 있습니다.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing a shape.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a rectangle.");
}
}
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
shape1.Draw(); // Drawing a circle.
shape2.Draw(); // Drawing a rectangle.
오버로딩 VS 오버라이딩
오버라이딩(Overriding)은 함수를 덮어쓰는거, 오버로딩(Overloading)은 함수를 불러오는거(여러가지)
제너릭
사용법
- 제너릭은 클래스나 메서드를 일반화시켜 다양한 자료형에 대응할 수 있는 기능입니다.
- 제너릭을 사용하면 코드의 재사용성을 높일 수 있습니다.
- C#에서는 <T> 형태의 키워드를 이용하여 제너릭을 선언합니다.
- 제너릭 클래스나 메서드에서 사용할 자료형은 선언 시점이 아닌 사용 시점에 결정됩니다.
- 제너릭 클래스나 메서드를 사용할 때는 <T> 대신 구체적인 자료형을 넣어줍니다.
// 제너릭 클래스 선언 예시
class Stack<T>
{
private T[] elements;
private int top;
public Stack()
{
elements = new T[100];
top = 0;
}
public void Push(T item)
{
elements[top++] = item;
}
public T Pop()
{
return elements[--top];
}
}
static void Main(string[] args)
{
// 제너릭 클래스 사용 예시
Stack<int> intStack = new Stack<int>();
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
Console.WriteLine(intStack.Pop()); // 출력 결과: 3
}
제너릭을 두개 이상 사용하는 예
class Pair<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
public void Display()
{
Console.WriteLine($"First: {First}, Second: {Second}");
}
}
static void Main(string[] args)
{
Pair<int, string> pair1 = new Pair<int, string>(1, "One");
pair1.Display();
Pair<double, bool> pair2 = new Pair<double, bool>(3.14, true);
pair2.Display();
}
출력
First: 1, Second: One
First: 3.14, Second: True
out, ref 키워드
사용법
- out, ref 키워드는 메서드에서 매개변수를 전달할 때 사용합니다.
- out 키워드는 메서드에서 반환 값을 매개변수로 전달하는 경우에 사용합니다.
- ref 키워드는 메서드에서 매개변수를 수정하여 원래 값에 영향을 주는 경우에 사용합니다.
- out, ref 키워드를 사용하면 메서드에서 값을 반환하는 것이 아니라, 매개변수를 이용하여 값을 전달할 수 있습니다.
// out 키워드 사용 예시
void Divide(int a, int b, out int quotient, out int remainder)
{
quotient = a / b;
remainder = a % b;
}
static void Main(string[] args)
{
int quotient, remainder; // 실제 변수
Divide(7, 3, out quotient, out remainder); // out 인자가 있는 변수
Console.WriteLine($"{quotient}, {remainder}"); // 출력 결과: 2, 1
}
out이라는 인자가 있는 변수는 실제 변수의 위치, 참조를 제공 해주기 때문에 Divide()의 매개변수에 있는 quotient는 Main에 있는 실제 quotient 변수를 직접 만진다.
게다가 Divide()에서 out 키워드를 가진 변수들은 초기화를 꼭 해준다. (초기화 안하면 에러)
이를 통해 깨닫는 사실은 "`Divide(7, 3, out quotient, out remainder);`가 실행되면 Main에 있는 quotient, remainder은 어떤값이든 담겨있겠구나" 라는 것이다.
// ref 키워드 사용 예시
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
static void Main(string[] args)
{
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 출력 결과: 2, 1
}
ref는 대입을 안한다고해서 에러가 나진 않는다.
위의 코드에서는 x, y를 참조로 a, b를 연결해준 것이다. 그래서 a, b가 변경이 되면 x, y에도 반영이 된다.
C언어의 포인터가 생각나네요~
주의 사항
값의 변경 가능성:
`ref` 매개변수를 사용하면 메서드 내에서 해당 변수의 값을 직접 변경할 수 있습니다. 이는 예기치 않은 동작을 초래할 수 있으므로 주의가 필요합니다.
성능 이슈:
`ref` 매개변수는 값에 대한 복사 없이 메서드 내에서 직접 접근할 수 있기 때문에 성능상 이점이 있습니다. 그러나 너무 많은 매개변수를 `ref`로 전달하면 코드의 가독성이 떨어지고 유지보수가 어려워질 수 있습니다. 적절한 상황에서 `ref`를 사용하는 것이 좋습니다.
변수 변경 여부 주의:
`out` 매개변수는 메서드 내에서 반드시 값을 할당해야 합니다. 따라서 `out` 매개변수를 전달할 때 해당 변수의 이전 값이 유지되지 않으므로 주의해야 합니다.
댓글