C# 종합 문법 3

김정환·2024년 9월 20일
0

클래스

객체지향의 특징

  1. 캡슐화

    • 관련된 데이터와 기능을 하나의 단위로 묶는 것
    • 클래스를 사용하여 정보를 은닉, 외부에서 직접적인 접근을 제한하여 안정성과 유지보수성을 높인다
  2. 상속

    • 두 클래스 간 공통되는 부분을 부모 클래스로 만들어 상속받아 중복을 줄이고 유지보수성을 높인다
  3. 다형성

    • 하나의 메서드를 클래스마다 다르게 작성해 필요한 기능을 구현할 수 있다.
  4. 추상화

    • 세부 구현을 감추고 핵심 개념에 집중하여 이해와 유지보수성을 높인다
  5. 객체

    • 객체 상태와 행동을 가지며 실제 개체나 개념을 모델링하여 모듈화와 재사용성을 높인다.

구성요소

  1. 필드 : 클래스에서 사용되는 변수
  2. 메서드 : 클래스에서 수행되는 동작
  3. 생성자 : 생성 시 호출되는 함수
  4. 소멸자 : 파괴 시 호출되는 함수

클래스

  1. 객체를 생성하기 위한 템플릿, 설계도, 붕어빵틀
  2. 객체를 만들기 위해선 클래스를 사용하여 인스턴스를 만든다.
  3. 속성(필드)과 동작(메서드)를 가진다.

객체

  1. 클래스의 인스턴스. 클래스의 실체화된 형태. 붕어빵
  2. 클래스로부터 생성되며, 각 객체는 독립적인 상태

클래스 선언과 인스턴스

  • 클래스는 데이터와 메서드를 하나로 묶는 사용자 정의 타입
  • 레퍼런스 타입

구조체 vs 클래스

  • 구조체와 클래스 모두 사용자 정의 형식
  • 구조체 : 값 형식 (밸류 타입) - 스택에 할당되고 복사할때 값이 복사
  • 클래스 : 참조 형식 (레퍼런스 타입) - 힙에 할당되고 참조로 전달되므로 성능에서 차이가 있음
  • 구조체는 상속을 받을 수 없지만 클래스는 단일 상속 및 다중 상속이 가능
  • 구조체는 작은 크기의 데이터 저장이나 단순한 데이터 구조에 적합
    클래스는 더 복잡한 객체를 표현하고 다양한 기능을 제공
Struct AAAA{
	~~
}

AAAA a; // 값 타입이기 때문에 그 자체로 값으로 사용할 수 있는 것.

class BBBB{
	~~
}

BBBB b = new BBBB(); // 레퍼런스 타입이기 때문에 메모리 공간을 만들고 값을 연결해주는 것.

접근 제한자

  • 필드, 메서드에 대해서 접근 가능 범위를 설정하는 것
    • public : 외부에서 자유롭게 접근 가능
    • private : 내부에서만 접근 가능
    • protected : 상속받은 클래스에서만 접근 가능

필드와 메서드

  • 필드 : 클래스 내 변수
    • 보통은 private로 만들고, 접근할 수 있는 함수나 프로퍼티를 제공
    • 함부로 접근하면 작동에 큰 영향을 미칠 수 있기 때문
  • 메서드 : 클래스 내 함수
    • 보통 public로 만들고, 외부에소 호출할 수 있게 함
Player p = new Player();
// Player p : 플레이어라는 객체를 담을 공간
// new Player() : 실제 플레이어 객체

생성자와 소멸자

생성자

  • 객체가 생성될때 호출되는 특별한 메서드
    • new 키워드와 함께 호출
    • 클래스와 동일한 이름
    • 반환 타입 없음
  • 객체 초기화 과정에서 필요한 작업을 수행
  • 오버로딩 가능
    기본적으로 매개변수가 없는 디폴트 생성자가 자동으로 생성됨.
    사용자가 따로 정의한 생성자가 없다면 디폴트 생성자는 생성되지 않음.

소멸자

  • 객체가 파괴, 소멸될 때 호출되는 특별한 메서드
    • 클래스와 동일한 이름을 가지며 이름 앞에 ~기호를 붙여 표현
    • 반환 타입 없음
  • 객체 사용이 종료되고 메모릭 해제될 때 자동으로 호출되어 필요한 정리 작업 수행
  • C#은 가비지 컬렉터가 메모리를 해제하므로 소멸자를 호출하는 것은 일반적으로 권장되지 않음
  • 오버로딩 불가능
  • 역할
    • 자원 해제: 파일핸들, 네트워크, DB 연결 등 외부 리소스 사용할 경우 소멸자로 리소스 해제 가능
    • 메모리 해제 : 객체가 사용한 메모리를 해제하고 관련된 자원 정리 가능
    • 로깅, 디버깅 : 소멸 시점에 로깅 작업이나 디버깅 정보 기록 가능

프로퍼티

  • 필드 값은 private로 선언됨
  • 외부에서 접근하려면 프로퍼티를 만들어서 접근할 수 있도록 제어함
  • 필드에 접근할 때 추가 작업이 가능함
[접근 제한자] [데이터 타입] 프로퍼티명{
	get {}
    set {}
}

class Person{
	private string _name;
    private float _hp;
    
    public float HealthPoint{
    	get { return _hp; }
        set {
        	_hp = value < 0f ? 0f : value;
        }
    }
}

접근 제한자 적용 & 유효성 검사

class Person{
	private string _name;
    private int _age;
    
    // 프로퍼티 자체에는 접근 가능하지만
    // set 부분은 내부에서만 가능하도록 제한한 경우
    public string Name{
    	get { return _name; }
    	private set { _name = value; }
    }
    
    // 유효성 검사
    public int Age{
    	get { return _age; }
        set {
        	if(value >= 0)
            	_age = value;
        }
    }
}

자동 프로퍼티

  • 따로 프로퍼티를 구현하지 않아도 자동으로 필드 역할까지 수행.
  • 자동 프로퍼티를 만들어두고 나중에 필요하면 분리해서 작성할 수 있음.
class Person{
	public string Name { get; set; }
    public int Age { get; set; }
    

tip

  • OOP 원칙을 지키도록 노력 -> 유지보수를 위한 방한
  • 프로퍼티를 사용해서 접근을 제어하면 코드 안정성과 가독성 향상 가능
  • 클래스 접근 제한자를 적절히 사용하여 필요한 부분만 외부에서 접근 가능하도록 설정하는 것이 좋음

상속과 다형성

상속

  • 부모 클래스로부터 확장하거나 재사용하기 위해 자식 클래스 / 하위 클래스를 생성함
  • 자식 클래스는 부모 클래스의 필드, 메서드를 상속받아 사용할 수 있다.
  • 상속으로 부모 클래스의 기능을 확장하거나 수정하여 새로운 클래스로 정의 가능

장점

  • 재사용성
  • 계층 구조 표현 가능
  • 유지보수성 향상

종류

  • 단일 상속
    • 하나의 부모 클래스만 상속 받는 것
    • C#은 단일 상속만 지원
  • 다중 상속
    • 여러 부모 클래스를 동시에 상속 받는 것
    • C#은 다중 상속을 지원하지 않음
  • 인터페이스 상속
    • 클래스가 인터페이스를 상속 받는 것
    • 인터페이스 다중 상속 가능
    • 하나의 클래스, 여러 인터페이스를 다중 상속 받는 것이 가능.

특징

  • 부모 클래스에 대한 멤버 접근
    • 부모 클래스의 멤버에 접근 가능하고 재사용 가능
  • 재정의
    • 부모 클래스의 메서드를 재정의하거나 확장 가능
  • 상속의 깊이
    • 단일 상속을 반복해서 상속 구조를 가질 수 있음
    • 깊어질 수록 관계가 복잡해지므로 적절한 깊이를 유지하고 적절하게 사용하는 것이 중요

접근 제한자와 상속

  • 접근 제한자는 상속 관계에서 중요
  • 부모 클래스 멤버의 접근 제한자에 따라 자식 클래스에서 멤버에 대한 접근 범위가 결정됨
  • 접근 제한자로 가시성을 조절해 캡슐화와 정보 은닉 구현 가능
public class Animal{
	public string Name { get; set; }
	public string Age { get; set; }
    
    public void Eat(){
    	Console.WriteLine("Animal is eating.");
    }
    public void Sleep(){
    	Console.WriteLine("Animal is sleeping.");
    }
}

public class Dog : Animal{
	
    public void Bark(){
    	Console.WriteLine("Dog barks.");
    }
} 

public class Cat : Animal {
	
    public void Meow(){
    	Console.WriteLine("Cat meows.");
    }
    
    // 부모 sleep보다 cat의 sleep을 먼저 사용
    // 이렇게 하기 보다는 virtual을 붙여서 override 하는 것이 더 바람직.
    // 이 방법은 다형성을 유지하는 방법은 아님. 그냥 재정의 방식임
    public void Sleep(){
    	Console.WriteLine("Cat is sleeping.");
    }
} 

다형성

가상 메서드

  • 부모 클래스에서 정의되고 자식 클래스에서 재정의됨
  • virtual 키워드를 사용하여 선언되며 자식 클래스에서 필요에 따라 재정의
  • 이 방식으로 자식 클래스는 부모 클래스를 확장하거나 재사용함
public class Unit
{
	// 자식에서 재정의 가능하도록 virtual 설정
    // virtual이 붙으면 실제 형태가 다를 수 있으니 실형태에 재정의 되었는지 확인하도록 함
    public virtual void Move(){
      Console.WriteLine("두발로 걷기");
    }
    public void Attack(){

      Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit
{
    // 저글링을 단일 형태로 쓰면 상관 없겠으나,
    // 이 클래스의 부모 클래스로 접근하여 사용할 때는 문제가 생길 수 있음.
    public void Move(){
    	Console.WriteLine("네발로 걷기");
    }
    
    // 이렇게 오버라이드하면 부모 클래스에서 호출해도 문제 없음.
	public override void Move(){
    	Console.WriteLine("네발로 걷기");
    }
    
}

static void Main(string[] args){
	List<Unit> units = new List<Unit>();
    
    units.Add(new Marine());
    units.Add(new Zergling());
    
    for(int i = 0; i < units.count; i++){
    	units[i].Move(); 
        // 결과 모두 두발 걷기 출력
        // 부모 클래스로 접근해서 부모 클래스의 Move()가 더 가까운 것.
        // override하면 저글링은 네발 걷기 출력
    }
}

추상 클래스

  • 직접적인 인스턴스를 생성할 수 없는 부모 클래스
  • 주 사용 방법은 베이스 클래스
  • abstract 키워드로 선언
  • 추상 메서드는 구현부가 없는 메서드로 자식 클래스에서 반드시 구현되어야 함. (인터페이스 같이)
    추상클래스를 상속받은 하위 클래스들은 무조건 추상 메서드가 있다는 것을 보장할 수 있음.
// 추상 클래스
public abstract class Shape{
	public abstract void Draw();
}

class Circle : Shape{

	// 반드시 구현해야하는 추상 메서드
	public override void Draw(){
    	Console.WriteLine("Drawing Circle.");
    }
}
class Square : Shape{

	// 반드시 구현해야하는 추상 메서드
	public override void Draw(){
    	Console.WriteLine("Drawing Square.");
    }
}

static void Main(string[] args){
	// Shape s = new Shape(); // 불가능
    
    List<Shape> shapes = new List<Shape>();
    
    shapes.Add(new Circle());
    shapes.Add(new Square());
    
    for(int i = 0; i < shapes.count; i++){
    	shapes[i].Draw(); 
    }
}

오버라이딩 vs 오버로드

  • 오버라이딩 overriding
    • 상속 관계에서 발생
    • 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의하는 것
    • 매개변수, 반환값 동일
  • 오버로드 overload
    • 같은 이름의 메서드를 매개변수의 타입, 순서, 종류에 따라 여러 개의 메서드로 정의하는 것
    • 같은 기능을 하는 메서드를 다른 방식으로 호출하는 것
      (다양한 매개변수의 조합으로 호출하는 것)
    • 매개변수 구성이 다름, 반환값 동일

고급 문법 및 기능

제너릭 Generic

  • 클래스나 메서드를 일반화시켜 다양한 자료형에 대응할 수 있는 기능
  • 코드를 하나만 짜놓고 다양한 자료형을 쓸 수 있게하는 것 = 재사용성
  • <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
}
  

예시 : 제너릭 2개 이상 사용

  • 클래스 내부에서 T1, T2로 구분해서 사용.
  • 다른 문자도 가능하지만 일반적으로 T or Tn 형태로 사용
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}");
    }
}
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();

out, ref

  • out, ref 메서드에서 매개변수를 참조하여 전달할 때 사용.
  • out : 반환값으로 받던 것들을 매개변수로 넘겨서 반환받을 수 있음.
    • out으로 받은 매개변수는 반드시 메서드에서 값을 초기화(변경)해주어야 한다.
  • ref : 매개변수를 수정하여 원래 값에 영향을 줄 수 있어 주의할 것.
    • ref로 받은 변수를 변경하지 않아도 에러가 나지 않음.
  • out, ref를 이용하면 메서드로 값을 반환하는 것이 아니라, 매개변수를 이용하여 값을 전달할 수 있다.
static void Divide(int a, int b, out int qu, out int remain){
	// a, b는 값을 복사해서 받는 것 -> 변경해도 원본 변수에 영향 x
    // out으로 받은 변수들은 참조해서 받은 것 -> 변경하면 원본 변수에도 반영
	qu = a / b;
    remain = a % b;
}

int quotient, remainder;
Divide(7, 3, out quotient, out remainder);
Console.WriteLine($"{quotient}, {remainder}"); // 출력 결과: 2, 1

// ref 키워드 사용 예시
void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 출력 결과: 2, 1

주의사항

  1. 값의 변경 가능성
    • ref 매개변수 사용 시 메서드 내에서 해당 변수의 값을 직접 변경할 수 있다.
      즉, ref 사용 시 값이 변조되었을 수 있다는 것을 생각해야한다.
  2. 성능 이슈
    • ref 매개변수는 복사가 없으므로 성능상으로 장점.
      단, 너무 많은 매개변수를 ref로 전달하면 코드의 가독성이 떨어지고 유지보수가 어려워진다.
      적절한 상황에서 ref를 사용하는 것이 좋다.
  3. 변수 변경 여부 주의
    • out은 반드시 메서드 내에서 값이 할당됨.
      out으로 변수를 전달할 때는 변수의 이전 값이 유지되지 않는다.
profile
만성피로 개발자

0개의 댓글