예외(Exception): 프로그램 실행 중에 발생하는 오류나 비정상적인 상황을 의미. 프로그램의 정상적인 흐름을 방해하며, 발생 즉시 프로그램이 종료되지 않도록 하기 위해 예외 처리를 통해 오류를 제어하고 대처함.

 

예외 처리의 필요성: 예외 처리는 프로그램 실행 중 발생할 수 있는 다양한 오류를 예측하고 대비할 수 있는 중요한 방법. 입력 오류, 파일 읽기/쓰기 오류, 네트워크 연결 문제, 숫자 계산 오류와 같은 다양한 예외가 발생할 수 있으며, 예외 처리를 통해 프로그램의 안정성과 사용자 경험을 개선할 수 있음.

 

예외 처리 구문(try-catch-finally)

try
{
    // 예외가 발생할 가능성이 있는 코드
}
catch (Exception ex)
{
    // 예외가 발생했을 때 처리하는 코드
}
finally
{
    // 예외 발생 여부와 관계없이 항상 실행되는 코드
}
  • try  블록: 예외가 발생할 가능성이 있는 코드를 작성하는 영역. 예외가 발생하면 프로그램의 흐름이 catch 블록으로 넘어감.
  • catch 블록: try 블록에서 예외가 발생하면 실행되는 블록. 예외의 종류에 따라 여러개의 catch 블록을 작성할 수 있음.
  • finally 블록: 예외 발생 여부와 관계없이 항상 실행되는 블록임. 주로 자원 해제(파일 닫기, 네트워크 연결 종료 등)에 사용됨.
//사용예시
try
{
    int number = int.Parse("abc"); // 정수가 아닌 문자열을 변환하려고 시도
}
catch (FormatException ex)
{
    Console.WriteLine("형식 오류: 정수로 변환할 수 없습니다.");
}
catch (Exception ex)
{
    Console.WriteLine($"예기치 않은 오류 발생: {ex.Message}");
}
finally
{
    Console.WriteLine("예외 처리 완료.");
}

 

throw 키워드: 예외를 강제로 발생시키거나 현재 예외를 다시 던질 때 사용.

//예외 발생시키기
throw new InvalidOperationException("잘못된 작업입니다.");
//예외 다시 던지기
try
{
    int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("0으로 나눌 수 없습니다.");
    throw; // 예외를 다시 던짐
}

 

예외의 종류: C#의 예외는 System.Exception 클래스를 기본으로 상속받으며, 다양한 예외 타입 존재.

  • System,Exception: 모든 예외의 기본 클래스
  • System.FormatException: 형식이 맞지 않는 경우 발생함. (예시: 문자열을 숫자로 변환할 때 형식이 맞지 않을 때)
  • System.NullReferenceException: 널 참조를 접근할 때 발생.
  • System.IndexOutOfRangeException: 배열 또는 리스트의 범위를 초과하여 접근할 때 발생.
  • System.DivideByZeroException: 0으로 나눌 때 발생.
  • System.InvalidOperationException: 잘못된 작업이 수행될 때 발생.

사용자 정의 예외: 기본 제공 예외 이외에도 사용자 정의 예외 클래스를 만들어 예외 처리를 세부적으로 할 수 있음. Exception 클래스를 상속받아 정의함.

public class CustomException : Exception
{
    public CustomException(string message) : base(message) { }
}

// 사용자 정의 예외 사용 예제
try
{
    throw new CustomException("사용자 정의 예외 발생");
}
catch (CustomException ex)
{
    Console.WriteLine($"CustomException: {ex.Message}");
}

 

장점

  • 프로그램의 안정성 향상: 예외 처리를 통해 오류 발생 시 프로그램이 예기치 않게 종료되지 않고 적절히 대처할 수 있음.
  • 코드의 가독성 향상: 오류를 예측하고 예외 처리 블록에 처리 로직을 작성하면 코드가 명확하고 이해하기 쉬워짐.
  • 자원 관리 용이: finally 블록을 사용하여 파일, 네트워크 연결 등의 자원을 안전하게 해제할 수 있음.

주의 사항

  • 불필요한 예외 처리 피하기: 예외 처리는 성능에 영향을 줄 수 있으므로, 예외가 발생할 가능성이 거의 없는 경우라면 불필요한 예외 처리를 피해야 함.
  • 구체적인 예외 처리: 구체적인 예외 타입으로 처리하여, 예상 가능한 오류에 대해 구체적으로 대응하는 것이 좋음.
  • 사용자 친화적인 메시지 제공: 예외 발생 시 사용자에게 명확한 메시지를 제공하여 이해를 도움.

 

예외 필터(Exception Filter): 특정 조건에 따라 예외를 처리할 수 있도록 catch 구문에 조건을 추가하는 기능. 이를 통해, 발생한 예외가 특정 조건을 만족할 때만 예외를 처리하거나, 조건을 만족하지 않는 예외는 다음 catch 블록이나 상위 예외 처리기로 넘길 수 있음.

 

예외 필터 구문: 예외 필터는 catch 블록에서 when 키워드와 조건식을 사용하여 정의.

 

try
{
    // 예외가 발생할 수 있는 코드
}
catch (Exception ex) when (ex.Message.Contains("특정 오류 메시지"))
{
    // 예외가 발생했고, 조건을 만족할 때만 실행되는 코드
}
//사용예시
try
{
    int number = int.Parse("abc"); // FormatException 발생
}
catch (FormatException ex) when (ex.Message.Contains("Input string was not in a correct format"))
{
    Console.WriteLine("형식 오류: 숫자 형식이 잘못되었습니다.");
}
catch (FormatException ex)
{
    Console.WriteLine("일반 형식 오류가 발생했습니다.");
}
catch (Exception ex)
{
    Console.WriteLine($"예기치 않은 오류 발생: {ex.Message}");
}

 

주의 사항

  • 성능에 영향: 예외 필터는 예외가 발생할 때 마다 조건으로 평가하므로, 복잡한 조건을 설정할 경우 성능에 영향을 줄 수 있음.
  • 예외 필터는 상태를 변경하지 않음: 예외 필터는 예외 조건을 검사할 때만 사용해야 하며, when 조건 내부에서 상태를 변경하지 않는 것이 좋음.
  • 조건 만족 여부에 따라 다음 catch 블록으로 이동: 예외 필터가 설정된 catch 블록의 조건이 만족되지 않으면, 다음 catch 블록으로 예외가 전달됨.

 

 

'2024-2 > C#' 카테고리의 다른 글

09.일반화 프로그래밍  (0) 2024.11.10
08.배열과 컬렉션, 인덱서  (0) 2024.11.10
07.프로퍼티  (0) 2024.11.10
06.인터페이스와 추상 클래스  (1) 2024.11.09
05.클래스  (2) 2024.11.09

 

일반화 메소드(Generic Method): 특정 데이터 타입에 의존하지 않고 다양한 타입으로 동작할 수 있도록 설계된 메소드. 메소드 선언 시점에 타입을 고정하지 않고, 호출할 때 타입을 지정함으로써 다양한 타입의 데이터를 처리할 수 있게 함.

 

일반화 메소드 정의 방법: 메소드 이름 앞에 타입 매개변수를 지정하여 정의.

public void MyGenericMethod<T>(T parameter)
{
    Console.WriteLine($"Parameter type: {typeof(T)}, Value: {parameter}");
}

-MyGennericMethodT라는 타입 매개변수를 가진 일반화 메소드, parameter는 어떤 타입이든 될 수 있음.

 

일반화 메소드와 반환 타입: 반환 타입에도 제네릭을 적용할 수 있음.이를 통해 입력받은 타입을 그대로 반환하거나, 특정 타입에 맞게 변환하여 반환.

public T GetDefaultValue<T>()
{
    return default(T); // 기본값 반환
}

// 사용 예제
int defaultInt = GetDefaultValue<int>(); // 0 반환
string defaultString = GetDefaultValue<string>(); // null 반환

 

제약 조건을 가진 일반화 메소드: 일반화 메소드에서 타입 매개변수에 제약 조건을 설정하여, 특정 조건을 만족하는 타입만 사용할 수 있게 함.

// where 제약 조건을 사용한 일반화 메소드
public void DisplayInfo<T>(T item) where T : IComparable
{
    Console.WriteLine($"Type: {typeof(T)}, Comparable: {item}");
}

 

장점

  • 코드 재사용성: 동일한 메소드를 다양한 타입으로 호출할 수 있어, 중복 코드를 줄이고 코드의 재사용성을 높임.
  • 타입 안전성: 일반화 메소드는 컴파일 시 타입이 결정되므로, 형 변환 오류를 방지하고 타입 안전성을 제공함.
  • 유연성: 메소드가 특정 타입에 종속되지 않고 다양한 타입과 함께 사용할 수 있으므로, 코드가 유연해짐.

일반화 메소드와 제네릭 클래스의 차이점

  • 제네릭 클래스: 클래스 전체에서 사용할 타입을 일반화하여 여러 메소드와 필드에 동일한 타입 매개변수를 사용함.
  • 일반화 메소드: 특정 메소드에만 일반화를 적용하여 다양한 타입을 받을 수 있게 하며 메소드 단위에서만 타입 매개변수를 정의함.

 

일반화 클래스(Generic Class): 특정 데이터 타입에 의존하지 않고 다양한 타입으로 동작할 수 있도록 설계된 클래스. 클래스 선언 시 타입을 고정하지 않고, 인스턴스를 생성할 때 타입을 지정할 수 있음. 타입 매개변수(T)를 사용하여 선언하며, 주로 컬렉션 클래스와 같은 데이터 저장 및 관리 용도의 클래스에서 자주 사용됨.

 

일반화 클래스의 정의 방법: 클래스 이름 옆에 타입 매개변수를 정의하여 선언.

public class GenericClass<T>
{
    private T item;

    public GenericClass(T value)
    {
        item = value;
    }

    public T GetItem()
    {
        return item;
    }
}

-GenericClass<T>는 타입 매개변수 T를 사용하여, 특정 타입에 종속되지 않는 일반화 클래스를 정의함.

 

일반화 클래스와 제네릭 컬렉션 클래스: 일반화 클래스는 제네릭 컬렉션 클래스에서 자주 사용됨. 다양한 데이터 타입을 저장할 수 있는 컬렉션을 구현할 때 유용함.  List<T>Dictionary<TKey, TValue>와 같은 제네릭 컬렉션이 일반화 클래스의 대표적인 예.

List<int> intList = new List<int> { 1, 2, 3 };
List<string> stringList = new List<string> { "Alice", "Bob", "Charlie" };

Dictionary<string, int> ageDictionary = new Dictionary<string, int>
{
    { "Alice", 30 },
    { "Bob", 25 }
};

 

일반화 클래스에 제약 조건 추가하기: 제약조건(Constraints)을 추가하여 특정 조건을 만족하는 타입만 사용할 수 있도록 제한할 수 있음. 제약 조건을 사용하면 타입 매개변수가 특정 인터페이스를 구현하거나 기본 생성자를 가지는지 등의 조건을 지정할 수 있음.

// where 제약 조건을 사용한 일반화 클래스
public class ComparableClass<T> where T : IComparable
{
    private T item;

    public ComparableClass(T value)
    {
        item = value;
    }

    public bool Compare(T other)
    {
        return item.CompareTo(other) == 0;
    }
}

-ComparableClass<T>T Icomparable 인터페이스를 구현하는 타입만 사용하도록 제한함. 이로 인해 compare 메소드에서 compareTo 메소드를 안전하게 호출할 수 있음.

 

일반화 클래스와 일반화 메소드의 차이점

  • 일반화 클래스: 클래스 전체에 타입 매개변수를 적용하여, 해당 클래스의 모든 메소드와 필드에서 같은 타입 매개변수를 사용할 수 있음.
  • 일반화 메소드: 특정 메소드에만 타입 매개변수를 적용하여, 메소드 단위에서 타입을 일반화함.

 

형식 매개변수 제약 조건: 제네릭 클래스나 메서드의 형식 매개변수에 특정 조건을 부여하여, 해당 제네릭이 특정 타입만 사용할 수 있도록 제한하는 기능.

 

종류

  1. 참조 타입 제약(class): 형식 매개변수가 참조 타입이여야 함.
  2. 값 타입 제약(struct): 형식 매개변수가 값 타입이여야 함.
  3. 기본 생성자 제약(new()): 형식 매개 변수가 매개변수가 없는 기본 생성자를 가져야 함.
  4. 특정 클래스 제약: 형식 매개변수가 특정 클래스이거나 해당 클래스에서 파생된 타입이여야 함.
  5. 특정 인터페이스 제약: 형식 매개변수가 특정 인터페이스를 구현해야 함.

제약 조건 사용 방법: where 키워드를 사용하여 지정함. 여러 제약 조건을 함께 사용할 수 있음.

public class GenericClass<T> where T : class, new()
{
    // T는 참조 타입이어야 하고 기본 생성자를 가져야 함
}

 

'2024-2 > C#' 카테고리의 다른 글

10.예외 처리하기  (0) 2024.11.10
08.배열과 컬렉션, 인덱서  (0) 2024.11.10
07.프로퍼티  (0) 2024.11.10
06.인터페이스와 추상 클래스  (1) 2024.11.09
05.클래스  (2) 2024.11.09

배열(Array): 동일한 타입의 여러 데이터를 하나의 변수로 묶어 관리할 수 있는 자료 구조. 고정된 크기를 가지며, 순차적으로 저장된 데이터에 인덱스를 통해 접근할 수 있음.

 

특징

  • 동일한 데이터 타입: 배열은 하나의 데이터 타입만 저장할 수 있어, 타입 안전성을 유지할 수 있음.
  • 고정된 크기: 배열의 크기는 초기화 시점에 고정되며, 이후에는 변경할 수 없음.
  • 인덱스를 통한 접근: 배열 요소는 0부터 시작하는 인덱스를 통해 개별적으로 접근할 수 있음.
  • 연속된 메모리: 배열 요소는 메모리에 연속적으로 저장되어, 데이터 접근 속도가 빠름.

배열의 선언과 초기화: 타입 뒤에 대괄호([])를 추가하여 선언하며, 초기화는 중괄호({})를 사용하거나 new 키워드를 사용하여 할 수 있음.

// 정수형 배열 선언과 초기화
int[] numbers = { 1, 2, 3, 4, 5 };

// 또는 new 키워드 사용
int[] scores = new int[5]; // 크기가 5인 배열 생성, 기본값 0으로 초기화
scores[0] = 10; // 첫 번째 요소에 값 설정
scores[1] = 20;

 

배열 요소 접근과 수정: 인덕스를 통해 접근하며, 인덕스를 통해 값을 수정할 수도 있음.

int[] numbers = { 1, 2, 3, 4, 5 };

// 배열 요소 접근
Console.WriteLine(numbers[0]); // 출력: 1
Console.WriteLine(numbers[2]); // 출력: 3

// 배열 요소 수정
numbers[2] = 10;
Console.WriteLine(numbers[2]); // 출력: 10

 

배열의 길이 확인: Length 속성을 사용하여 확인할 수 있음.

int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Length); // 출력: 5

 

 

다차원 배열: 배열은 2차원 이상의 다차원 배열로 선언할 수 있음. 다차원 배열은 행과 열로 이루어지며, 2차원 배열은 주로 테이블 형태의 데이터를 저장할 때 유용함.

// 2차원 배열 선언 및 초기화
int[,] matrix = { { 1, 2 }, { 3, 4 }, { 5, 6 } };

// 요소 접근
Console.WriteLine(matrix[0, 0]); // 출력: 1
Console.WriteLine(matrix[2, 1]); // 출력: 6

 

장점과 단점

  • 빠른 데이터 접근: 배열 요소는 인덱스를 통해 빠르게 접근할 수 있음.
  • 메모리 효율성: 배열은 메모리에 연속적으로 저장되므로, 데이터 접근과 관리를 효율적으로 수행할 수 있음.
  • 반복문을 통한 처리 용이: 배열은 반복문을 통해 모든 요소에 쉽게 접근하고 처리할 수 있어 유용함.
  • 고정된 크기: 배열의 크기는 초기화 시점에 고정되며, 이후에는 변경 불가.
  • 동일한 타입만 저장: 배열은 동일한 타입의 데이터만 저장할 수 있어, 다양한 타입을 한 배열에 저장하기 어려움.
  • 삽입과 삭제의 비효율성: 배열 중간에 요소를 삽입하거나 삭제하는 경우, 요소들을 이동해야하므로 성능이 저하될 수 있음.

 

System.Array: C#에서 모든 배열의 기본 클래스, 배열을 관리하고 조작할 수 있는 여러 메소드와 속성을 제공하는 클래스.

 

특징

  • 모든 배열의 기본 클래스: System.Array는 모든 배열이 상속하는 클래스. 배열 생성할때마다 이 클래스의 기본 메소드와 속성 사용 가능.
  • 배열 조작 기능 제공: 정렬, 검색, 복사, 초기화 등의 기능을 제공함.
  • 정적 메소드로 사용: 대부분의 기능은 System.Array 클래스의정적 메소드를 통해 바로 호출할 수 있으며, 특정 배열 인스턴스를 필요로 하지 않음.

속성

1.Length 속성: 배열의 총 길이를 반환함.

int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Length); // 출력: 5

2.GetLength 메소드: 특정 차원의 길이 반환. 다차원 배열에서 각 차원의 길이를 구할 때 유용함.

int[,] matrix = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
Console.WriteLine(matrix.GetLength(0)); // 출력: 3 (행의 개수)
Console.WriteLine(matrix.GetLength(1)); // 출력: 2 (열의 개수)

3.Clear 메소드: 배열의 모든 요소를 기본값으로 초기화함.

int[] numbers = { 1, 2, 3, 4, 5 };
Array.Clear(numbers, 0, numbers.Length); // 모든 요소를 0으로 초기화

4.Copy 메소드: 하나의 배열을 다른 배열에 복사함.

int[] source = { 1, 2, 3 };
int[] destination = new int[3];
Array.Copy(source, destination, source.Length);

foreach (var item in destination)
{
    Console.WriteLine(item); // 출력: 1, 2, 3
}

5.Sort 메소드: 배열의 요소를 오름차순으로 정렬함.

int[] numbers = { 3, 1, 4, 1, 5 };
Array.Sort(numbers);

foreach (var number in numbers)
{
    Console.WriteLine(number); // 출력: 1, 1, 3, 4, 5
}

6.Reverse 메소드: 배열의 요소를 역순으로 정렬함.

int[] numbers = { 1, 2, 3, 4, 5 };
Array.Reverse(numbers);

foreach (var number in numbers)
{
    Console.WriteLine(number); // 출력: 5, 4, 3, 2, 1
}

7.IndexOf 메소드: 특정 요소의 인덕스를 반환함. 요소가 없으면 -1을 반환함.

int[] numbers = { 1, 2, 3, 4, 5 };
int index = Array.IndexOf(numbers, 3);
Console.WriteLine(index); // 출력: 2

 

장점과 한계

  • 다양한 배열 조작 기능: 배열의 정렬, 검색, 복사, 초기화 등 다양한 기능을 제공해 배열을 쉽게 다룰 수 있음.
  • 다차원 배열 지원: 단일 배열뿐만 아니라 다차원 배열에 대해서도 다양한 조작이 가능함.
  • 정적 메소드 사용의 편리함: 대부분의 메소드가 정적 메소드이므로 객체 생성 없이도 배열을 처리할 수 있음.
  • 크기 변경 불가: System.Array는 제네릭 컬렉션이 아니기 때문에, 컬렉션과 달리 타입 안전성이 제한될 수 있음.
  • 제너릭 미지원: System.Array는 제네릭 컬렉션이 아니기 때문에, 컬렉션과 달린 타입 안전성이 제한될 수 있음.

 

컬렉션(Collection): 데이터를 그룹으로 묶어 관리하고 조작을 할 수 있는 자료 구조.동적 크기 조절이 가능하여, 배열과 달린 크기를 고정하지 않고 데이터 추가 및 삭제가 가능함.

 

특징

  • 동적 크기 조절: 컬렉션은 필요에 따라 자동으로 크기를 조정하여 데이터를 저장할 수 있음.
  • 다양한 자료 구조 지원: 리스트, 큐, 스택, 딕셔너리 등 다양한 자료 구조를 제공하여, 데이터의 삽입, 삭제, 검샥, 정렬 등을 용이하게 함.
  • 제네릭과 비 제네릭 지원: 제네릭 컬랙션을 사용하면 데이터 타입을 미리 지정할 수 있어, 타입 안전성과 성능을 높일 수 있음.

종류

1.제네릭 컬렉션(System.Collection.Generic): 특정 데이터 타입을 지정할 수 있는 컬렉션으로, 컴파일 시점에 타입이 고정되므로 타입 안전성을 제공하고 성능이 우수함. 형 변환이 필요없고, 컬렉션 내의 데이터 타입이 일관되게 유지됨.

  • List<T>: 가변 크기의 리스트. 데이터 추가, 삭제가 가능하며 인덱스를 통해 접근 할 수 있음.
List<int> numbers = new List<int> { 1, 2, 3 };
numbers.Add(4); // 요소 추가
numbers.Remove(2); // 요소 제거
  • Dictionary<TKey, TValue>: 키와 값이 쌍으로 데이터를 저장하며, 키를 통해 값에 접근할 수 있음.
Dictionary<string, int> ages = new Dictionary<string, int> { { "Alice", 30 }, { "Bob", 25 } };
Console.WriteLine(ages["Alice"]); // 출력: 30
  • HashSet<T>: 중복 없는 요소를 저장하는 집합 컬랙션. 요소의 중복을 허용하지 않음.
HashSet<int> uniqueNumbers = new HashSet<int> { 1, 2, 3, 3 };
  • Queue<T>:선입선출(FIFO)방식으로 요소를 처리하는 큐 자리 구조임.
Queue<string> queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Dequeue(); // "First"를 제거
  • Stack<T>: 후입선출(LIFO)방식으로 요소를 처리하는 스택 자료 구조임.
Stack<string> stack = new Stack<string>();
stack.Push("First");
stack.Push("Second");
stack.Pop(); // "Second"를 제거

 

2.비제네릭 컬렉션(System.Collections): 객체 형식으로 데이터 저장. 데이터 접근 시 형 변환이 필요하며, 타입 안전성이 떨어짐.

  • ArrayList: 크기가 가변적인 배열,다양한 타입의 데이터를 저장할 수 있음
ArrayList list = new ArrayList { 1, "Hello", true };
list.Add(3.14);
  • Hashtable: 키-값 쌍으로 데이터를 저장하는 컬렉션으로, 제네릴 딕셔너리와 유사하지만 타입 안전성을 제공하지 않음.
Hashtable table = new Hashtable();
table["Alice"] = 30;
table["Bob"] = 25;
  • QueueStack: 비제네릭 컬렉션으로, Queue<T>Stack<T>의 제네릭 버전이 없던 시절에 주로 사용됨.

 

인덱서(Indexer): 클래스나 구조체가 배열처럼 인덱스를 통해 개별 요소에 접근할 수 있게 해주는 특수한 속성. 일반 속성과 비슷하지만, 매개변수를 받아 특정 위치의 데이터를 반환하거나 설정할 수 있다는 점에서 차이가 있음.

 

인덱스 구조: this 키워드와 대활호([])를 사용하여 정의. getset 접근자를 포함할 수 있음

public class SampleCollection
{
    private int[] numbers = new int[10]; // 내부 배열

    // 인덱서 정의
    public int this[int index]
    {
        get { return numbers[index]; } // 인덱스를 통해 값 읽기
        set { numbers[index] = value; } // 인덱스를 통해 값 설정
    }
}
//사용예시
SampleCollection collection = new SampleCollection();

// 인덱스를 통해 데이터 설정
collection[0] = 10;
collection[1] = 20;

// 인덱스를 통해 데이터 읽기
Console.WriteLine(collection[0]); // 출력: 10
Console.WriteLine(collection[1]); // 출력: 20

 

장점

  • 배열처럼 객체를 사용 가능: 객체를 배열처럼 다룰 수 있어 코드가 직관적이고 간결해짐.
  • 데이터 캡슐화: 내부 데이터를 외부에서 직접 접근하지 않고 인덕스를 통해 안전하게 접근할 수 있음.
  • 유연한 데이터 접근: 객체에 포함된 데이터를 특정 조건에 따라 유연하게 접근하고 수정할 수 있음.

인덱서와 속성의 차이점

  • 인덱서는 매개변수를 통해 데이터를 관리할 수 있어 다수의 데이터를 다룰 때 유리함.
  • 속성은 특정 필드에 대한 접근을 제어하여, 보통 단일 값을 반환하거나 설정하는데 사용됨.
  • 인덱서는 이름이 없고 this 키워드를 사용하지만, 속성은 이름을 가지며 개별적으로 정의됨.
public class Student
{
    private Dictionary<string, int> scores = new Dictionary<string, int>();

    // 인덱서: 과목 이름을 통해 점수를 설정하고 가져온다.
    public int this[string subject]
    {
        get { return scores.ContainsKey(subject) ? scores[subject] : 0; }
        set { scores[subject] = value; }
    }

    // 속성: 학생의 이름을 설정하고 가져온다.
    public string Name { get; set; }
}
//사용예시
Student student = new Student();
student.Name = "Alice";  // 속성을 통해 이름 설정
student["Math"] = 95;    // 인덱서를 통해 점수 설정

Console.WriteLine($"Name: {student.Name}, Math Score: {student["Math"]}");
// 출력: Name: Alice, Math Score: 95

 

 

foreach 문: 컬렉션 또는 배열의 모든 요소를 순회하면서 각 요소에 접근할 수 있게 해주는 반복문. 인덱스나 조건을 사용하지 않고, 컬렉션의 처음부터 끝까지 모든 요소를 자동으로 순회하기 때문에 코드가 간결하고 가독성이 좋음. 배열, 리스트, 딕셔너리등 다양한 컬렉션과 함께 사용할 수 있으며, 내부적으로 컬렉션의 각 요소에 대해 IEnumerator 인터페이스를 사용하여 반복을 수행함.

 

foreach 문의 구조

foreach (var item in collection)
{
    // 컬렉션의 각 요소에 대해 실행할 코드
}

 

  • var item: 컬렉션의 각 요소가 저장될 변수. var를 사용하면 컴파일러가 자동으로 요소의 타입을 추론하며, 요소의 타입을 명시적으로 지정할 수도 있음.
  • in collection: 반복할 대상 컬렉션 또는 배열.
  • 반복문 블록: 컬렉션의 각 요소에 대해 실행할 코드를 정의.
int[] numbers = { 1, 2, 3, 4, 5 };

foreach (int number in numbers)
{
    Console.WriteLine(number); // 각 요소를 출력
}

 

foreach 문의 제한 사항

  • 요소 수정 불가: foreach 문에서는 컬레션의 요소를 수정할 수 없음. 수정이 필요한 경우 for문 사용.
  • 모든 요소 순회: foreach 문은 걸렉션의 모든 요소를 순회하므로, 특정 조건에 따라 중간에 반복을 종료하거나 요소를 건너뛰기 어려움.
  • 컬렉션 수정 제한: foreach 문을 사용하는 동안 컬렉션의 크기를 변경(요소 추가/삭제)하면 오류가 발생함.

 

 

'2024-2 > C#' 카테고리의 다른 글

10.예외 처리하기  (0) 2024.11.10
09.일반화 프로그래밍  (0) 2024.11.10
07.프로퍼티  (0) 2024.11.10
06.인터페이스와 추상 클래스  (1) 2024.11.09
05.클래스  (2) 2024.11.09

 

프로퍼티: 클래스의 필드에 접근하고 값을 설정하는 방법을 제공하는 멤버로, 필드에 대한 접근 제어를 가능하게 하여 데이터의 캡슐화를 구현하는데 사용.

 

프로퍼티의 구조

public class Person
{
    private string name; // 필드

    public string Name // 프로퍼티
    {
        get { return name; }  // 값 읽기
        set { name = value; } // 값 설정
    }
}

 

  • get 접근자: 프로퍼티 값을 읽을 때 호출.
  • set 접근자: 프로퍼티 값을 설정할 때 호출.
Person person = new Person();
person.Name = "Alice"; // set 접근자 호출
Console.WriteLine(person.Name); // get 접근자 호출

 

 

자동 구현 프로퍼티(Auto-Implemented Property): 컴파일러가 자동으로 private 필드를 생성, getset 접근자만으로 프로퍼티를 정의할 수 있음.

public class Person
{
    public string Name { get; set; } // 자동 구현 프로퍼티
}

-Name은 자동 구현 프로퍼티, 컴파일러가 자동으로 private 필드를 생성함.

 

읽기 전용 및 쓰기 전용 프로퍼티

  • 읽기 전용 프로퍼티: get 접근자만을 정의하여 읽기만 가능하게 할 수 있음.
  • 쓰기 전용 프로퍼티: set 접근자만을 정의하여 쓰기만 가능하게 할 수 있음.
public class Product
{
    public string Name { get; } // 읽기 전용 프로퍼티
    public decimal Price { private get; set; } // 쓰기 전용 프로퍼티 (외부에서 get 접근 제한)

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}

 

프로퍼티를 활용해 특정 필드의 값에 접근할 때 유효성 검사를 추가할 수 있음. (예시: 나이값이 0보다 작을 경우 설정하지 않도록 제어)

public class Person
{
    private int age;
    
    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0)
            {
                age = value;
            }
            else
            {
                Console.WriteLine("나이는 0 이상이어야 합니다.");
            }
        }
    }
}

 

장점

  • 데이터 캡슐화: 필드에 직접 접근하지 못하도록 하고, 프로퍼티를 통해서만 접근하게 하여 데이터의 무결성을 유지할 수 있음.
  • 필드에 대한 제어 제공: 프로퍼티를 통해 필드 값을 읽거나 설정할 때 유효성 검사나 특정 로직을 추가할 수 있음.
  • 간결한 구문: 자동 구현 프로퍼티를 통해 간결하게 필드를 정의하고 사용할 수 있음.

 

프로퍼티 초기화: 객체를 생성할 때 프로퍼티에 초기값을 설정하는 방식. 코드의 가독성과 효율성을 높여줌.자동 구현 프로퍼티와 객체 초기화 구문(Object Initializer Syntax)을 사용해 쉽게 초기화할 수 있음.

 

자동 구현 프로퍼티의 초기화: 자동 구현 프로퍼티를 사용할 때 초기값을 지정할 수 있음. 이 방법은 프로퍼티 선언 시 기본값을 설정할 수 있어 객체 생성 시점에 값을 자동으로 초기화하는데 유용함.

public class Person
{
    // 초기값을 설정한 자동 구현 프로퍼티
    public string Name { get; set; } = "Unknown";
    public int Age { get; set; } = 18;
}

Person person = new Person();
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 출력: Name: Unknown, Age: 18

 

생성자를 통한 초기화: 생성자를 통해 객체를 생성하면서 프로퍼티의 값을 설정할 수 있음. 생성자를 이용한 초기화는 필수적인 갓이나 특정 논리가 필요할 때 유용함.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    // 생성자를 통한 초기화
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

Person person = new Person("Alice", 30);
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 출력: Name: Alice, Age: 30

 

객체 초기화 구문(Object Initializer Syntax): 생성자를 호출하지 않고도 프로퍼티를 초기화 할 수 있음. 객체 생성과 동시에 프로퍼티 값을 설정할 수 있어 간결하고 직관적임.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// 객체 초기화 구문을 사용한 초기화
Person person = new Person
{
    Name = "Alice",
    Age = 30
};

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 출력: Name: Alice, Age: 30

//생성자와 함께 사용한 객체 초기화 구문
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string City { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

// 생성자와 객체 초기화 구문을 함께 사용
Person person = new Person("Alice", 30)
{
    City = "Seoul"
};

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}, City: {person.City}");
// 출력: Name: Alice, Age: 30, City: Seoul

 

초기화 전용 구현 프로퍼티(Init-Only Auto-Implemented Property): 객체가 생성될 때 초기화만 할 수 있고, 이후에는 수정할 수 없는 프로퍼티.

 

특징

  • init 접근자: set 대신 init 접근자를 사용하여 프로퍼티를 선언하면, 객체 초기화 시점에만 값을 설정할 수 있음. 이후에는 값을 변경할 수 없어, 불변(Immutable) 객체를 구현하는데 유용함.
  • 객체 초기화 구문(Object Initializer Syntax)과 함께 사용: 생성자 호출 후, 객체 초기화 구문을 통해 초기값을 설정할 수 있음.
  • 읽기 전용의 유연성 제공: readonly 필드처럼 초기화 이후 수정이 불가능하지만, 객체 초기화 구문을 통해 더 유연하게 값을 설정 할 수 있음.
public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

Person person = new Person
{
    Name = "Alice",
    Age = 30
};

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

// 초기화 후에는 수정 불가
// person.Name = "Bob"; // 오류 발생: init-only 프로퍼티는 수정할 수 없음

//생성자와 함께 사용
public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
    public string City { get; init; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

// 생성자와 객체 초기화 구문을 함께 사용
Person person = new Person("Alice", 30)
{
    City = "Seoul"
};
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}, City: {person.City}");

 

 

프로퍼티 초기화 장점

  • 간결하고 직관적인 코드: 객체 생성과 동시에 프로퍼티를 초기화할 수 있어 코드가 간결해짐.
  • 필수값과 선택값 구분: 생성자와 객체 초기화 구문을 함께 사용하면 필수 프로퍼티와 선택적 프로퍼티를 구분하여 초기화할 수 있음.
  • 코드 가독성 향상: 객체 생성과 초기화 과정을 한눈에 볼 수 있어 코드의 가독성이 높아짐.

 

불변 객체(Immutable Object): 생성된 이후 내부 상태가 절대로 변경되지 않는 객체. 객체의 속성이나 필드의 값을 객체가 생성된 이후에는 수정할 수 없는 것이  특징임.

 

특징

  • 생성 이후 수정 불가: 불변 객체는 생성 시점에만 값을 설정, 이후에는 값을 변경할 수 없음.
  • 안정성 제공:불변 객체는 변경되지 않기 때문에, 참조를 공유할 때 값이 바뀌는 일이 없어 안정적임.
  • 스레드 안전성: 불변 객체는 변경할 수 없으므로, 멀티스레드 환경에서 여러 스레드가 동시에 접근하더라도 안전하게 사용할 수 있음.
  • 가비지 컬렉션 효율성: 불변 객체는 변경되지 않으므로, 새로운 값이 필요할 때 마다 기존 객체를 수정하는 대신 새로운 객체를 생성하게 됨. (가비지 컬렉션에서 관리하기 용이)

구현 방법

 

  • readonly 필드를 사용한 불변 객체: readonly 키워드를 사용하면 필드가 초기화 이후 변경되지 않도록 강제할 수 있음.
public class ImmutablePerson
{
    public readonly string Name;
    public readonly int Age;

    public ImmutablePerson(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

 

  • init 접근자를 사용한 불변 객체: 프로퍼티가 객체 초기화 시점에만 설정되도록 할 수 있음. 이를 통해 생성자뿐 아니라 객체 초기화 구문으로도 불변 객체를 쉽게 생성할 수 있음.
public class ImmutablePerson
{
    public string Name { get; init; }
    public int Age { get; init; }
}

ImmutablePerson person = new ImmutablePerson { Name = "Alice", Age = 30 };

// 초기화 이후 값 변경 불가
// person.Name = "Bob"; // 오류 발생

 

  • 레코드 타입을 사용한 불변 객체: 레코드는 불변 객체를 쉽게 생성할 수 있는 기능 제공함. 주로 데이터 전용 클래스에서 사용함.
public record ImmutablePerson(string Name, int Age);

ImmutablePerson person = new ImmutablePerson("Alice", 30);
Console.WriteLine(person.Name); // 출력: Alice

// 불변 객체이므로 기존 객체를 수정하는 대신 새로운 객체를 생성해야 함
ImmutablePerson newPerson = person with { Name = "Bob" };
Console.WriteLine(newPerson.Name); // 출력: Bob
Console.WriteLine(person.Name); // 출력: Alice

 

장점과 단점

  • 코드 안정성: 객체의 상태가 변경되지 않으므로, 코드를 예측 가능하고 안정적으로 유지할 수 있음.
  • 스레드 안전성: 불변 객체는 변경할 수 없기 때문에, 여러 스레드가 동시에 접근해도 데이터 일관성 유지.
  • 가독성 및 유지보수성: 불변 객체는 초기화 이후 상태가 변하지 않으므로, 추적과 디버깅이 쉬워지고 유지보수 용이.
  • 성능 오버헤드: 값을 변경할 때마다 새로운 객체를 생성해야 하므로, 메모리와 성능에 영향을 줄 수 있음.
  • 복잡한 데이터 구조 처리의 어려움: 불변 객체를 사용하면 중첩된 데이터 구조를 변경할 때마다 전체 구조를 복사해야 할 수 있어 다루기 복잡해질 수 있음.

 

레코드(Record): C# 9.0에서 도입된 새로운 참조 타입, 주로 데이터 중심의 불변 객체를 간단하게 표현할 수 있도록 설계됨. 객체 상태를 쉽게 비교하고 복사할 수 있으며, 불변성을 기본으로 하여 데이터 모델링이나 데이터 전송에 적합함. class와 같은 참조 타입이지만, 데이터 전용 타입에 특화된 몇 가지 기능을 제공함.

 

특징

  • 불변성: 레코드는 기본적으로 불변 객체로 생성됨. 생성 시 값이 설정되며, 이후에는 변경할 수 없음.
  • 값 비교: 레코드는 값 기반 비교를 지원함. 따라서 두 레코드 객체의 모든 프로퍼티 값이 동일하다면 같은 값으로 간주.
  • 데이터 모델링에 최적화: 데이터 구조를 정의할 때, EqualsGetHashCode 메소드가 자동으로 구현되어 있어 데이터 모델링이 간단함.
  • whit 식을 통한 복사: 기존 레코드를 기반으로 일부 값만 변경하여 새 레코드를 쉽게 만들 수 있음.

레코드 선언:record 키워드를 사용, 위치 기반 생성자를 통해 자동으로 생성자를 정의할 수 있음.

public record Person(string Name, int Age);
//레코드 생성 및 값 비교
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);

Console.WriteLine(person1 == person2); // 출력: True (값이 동일하면 같은 값으로 간주)

 

레코드 불변성: 레코드는 기본적으로 불변 객체로 설계되어 있기 때문에, 생성 이후에 값을 변경할 수 없음. 값을 변경하려면 with 식을 사용하여 새 레코드를 생성해야 함. 이는 불변 객체의 특성을 유지하면서, 필요한 경우 특정 프로퍼티만 수정할 수 있는 방법 제공함.

Person person1 = new Person("Alice", 30);
Person person2 = person1 with { Age = 31 }; // 새로운 레코드를 생성하면서 Age만 변경

Console.WriteLine(person1.Age); // 출력: 30
Console.WriteLine(person2.Age); // 출력: 31

 

장점

  • 값 기반 비교 지원: 레코드는 값이 동일하면 동일한 객체로 간주되어 값 비교가 필요한 데이터 전용 객체를 구현하는데 적합함.
  • 불변 데이터 모델링에 최적화: 불변 객체로 설계되어 데이터 전송 및 공유 시 안전함.
  • 간결한 구문: 데이터 전용 객체를 선언할 때 생성자, Equals, GetHashCode가 자동으로 생성되어 코드가 간결해짐.

레코드와 클래스의 차이점

특성 레코드(Record) 클래스(Class)
기본 비교 방식 값 기반 비교(==Equals 재정의) 참조 기반 비교
불변성 기본적으로 불변(immutable) 기본적으로 가변(mutable)
주 용도 데이터 모델링, 데이터 전송 객체(DTO) 모든 종류의 객체 모델링
with식 지원 지원(with 키워드를 통해 복사 및 수정) 지원되지 않음
자동 멤버 구현 생성자, Equals,GetHashCode 자동 생성 자동 구현 없음.

 

 

무명 형식(Anonymous Type): C#에서 클래스나 구조체 없이 간단하게 여러 속성을 가진 객체를 정의하는 방법. new{ } 구문을 사용하여 생성하며, 컴파일러 자동으로 형식을 정의하고, 속성 이름과 값을 기반으로 타입을 추론함.

 

특징

  • 형식 이름이 없음: 무명 형식에는 별도의 클래스 이름이 없고, 런타임에서 컴파일러가 자동으로 이름 없는 형식을 생성함.
  • 읽기 전용 속성: 무명 형식의 속성은 기본적으로 읽기 전용임. 한 번 값을 설정하면 이후 변경할 수 없음.
  • 자동 타입 추론: 속성의 타입은 컴파일러가 자동으로 추론하며, var 키워드를 사용해 변수의 타입을 지정함.
  • 주로 LINQ에서 활용: 무명 형식은 LINQ 쿼리 결과를 임시로 저장하는데 자주 사용됨.
var person = new { Name = "Alice", Age = 30 };

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 출력: Name: Alice, Age: 30
//무명 형식과 LINQ
var people = new[]
{
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 35 }
};

var selectedPeople = from person in people
                     where person.Age > 28
                     select new { person.Name, person.Age };

foreach (var p in selectedPeople)
{
    Console.WriteLine($"Name: {p.Name}, Age: {p.Age}");
}
// 출력:
// Name: Alice, Age: 30
// Name: Charlie, Age: 35

 

장점과 한계

  • 간단한 데이터 구조 생성: 클래스를 정의할 필요 없이 간단하게 데이터 구조를 생성할 수 있어 코드가 간결해짐.
  • 임시 데이터 저장에 적합: 일회성 데이터 저장에 유용하여, 특정 작업에서만 필요한 임시 객체를 쉽게 생성할 수 있음.
  • 타입 추론의 편리함: var 키워드를 사용하여 컴파일러가 타입 추론하므로, 타입을 명시적으로 지정할 필요 없음.
  • 확장 불가: 무명 형식은 정의 후 확장하거나 속성의 값을 변경할 수 없음.
  • 타입의 명확성 부족: 무명 형식의 타입 이름이 없기 때문에, 특정 함수에서 무명 형식을 반환하는 것은 불가능함.
  • 클래스나 구조체와 비교하여 유연성 부족: 무명 형식은 데이터가 고정되어 있어, 클래스를 사용하는 것보다 유연성이 떨어짐.

 

'2024-2 > C#' 카테고리의 다른 글

09.일반화 프로그래밍  (0) 2024.11.10
08.배열과 컬렉션, 인덱서  (0) 2024.11.10
06.인터페이스와 추상 클래스  (1) 2024.11.09
05.클래스  (2) 2024.11.09
04.메소드  (0) 2024.11.08

 

인터페이스(Interface): 클래스나 구조체가 반드시 구현해야 하는 멤버의 목록을 정의하는 추상적인 타입. 메소드, 속성, 이벤트, 인덱스 등의 선언만 포함하며, 실제 구현은 인터페이스를 구현하는 클래스나 구조체에서 수행함. 인터페이스를 통해 클래스는 일관된 규칙을 따르도록 강제되며, 다형성을 지원해 코드의 확장성과 유연성을 높임.

 

특징

  • 멤버 선언만 포함: 인터페이스에는 메소드, 속성, 이벤트, 인덱서의 선언만 포함되며, 구현은 포함되지 않음.
  • 다중 상속 지원: 클래스의 다중 상속이 불가능하지만, 인터페이스는 여러 개를 동시에 구현할 수 있음.
  • 계약(contract) 역할: 인터페이스는 특정 기능을 제공하겠다는 계약을 의미, 인터페이스를 구현한 클래스는 인터페이스의 모든 멤버를 구현해야 함.
  • 다형성 지원: 인터페이스를 통해 다양한 클래스가 동일한 방식으로 사용될 수 있어, 코드의 유연성과 유지보수성이 높아짐.
//인터페이스 선언
public interface IMovable
{
    void Move(int distance);
    string Direction { get; set; }
}
//인터페이스 구현
public class Car : IMovable
{
    public string Direction { get; set; }

    public void Move(int distance)
    {
        Console.WriteLine($"자동차가 {Direction} 방향으로 {distance}km 이동했습니다.");
    }
}

public class Bicycle : IMovable
{
    public string Direction { get; set; }

    public void Move(int distance)
    {
        Console.WriteLine($"자전거가 {Direction} 방향으로 {distance}km 이동했습니다.");
    }
}

 

장점

  • 유연한 설계: 인터페이스를 통해 서로 다른 클래스들이 동일한 규격을 따르도록 하여, 코드의 일관성과 유연성을 높임.
  • 다중 상속 가능: 클래스 상속의 한계를 보완하여, 하나의 클래스가 여러 인터페이스를 구현할 수 있음.
  • 다형성: 인터페이스를 통해 다형성을 구현할 수 있어, 동일한 방식으로 다양한 객체를 처리할 수 있음.
  • 코드 유지보수성 향상: 인터페이스를 사용하면 코드의 구조를 명확해져 유지보수가 용이함.

 

인터페이스 상속: 다른 인터페이스를 상속할 수 있음. 인터페이스 간에 기능을 확장할 때 유용하며, 특정 인터페이스에 더 많은 기능을 추가할 수 있음. 클래스 상속과 달리, 다중 상속이 가능하며 한 인터페이스가 여러 인터페이스를 상속할 수 있음. 상속된 인터페이스는 상속받은 모든 인터페이스의 멤버를 포함하므로, 이를 구현하는 클래스는 상속받은 인터페이스의 모든 멤버를 구현해야 함.

public interface IMovable
{
    void Move(int distance);
}

public interface IRotatable
{
    void Rotate(int angle);
}

// IMovable과 IRotatable을 상속하여 기능을 확장한 인터페이스
public interface ITransformable : IMovable, IRotatable
{
    void Scale(double factor);
}
//상속된 인터페이스 구현 예제
public class TransformableObject : ITransformable
{
    public void Move(int distance)
    {
        Console.WriteLine($"객체가 {distance}만큼 이동했습니다.");
    }

    public void Rotate(int angle)
    {
        Console.WriteLine($"객체가 {angle}도 만큼 회전했습니다.");
    }

    public void Scale(double factor)
    {
        Console.WriteLine($"객체가 {factor}배로 확대/축소되었습니다.");
    }
}

 

장점

  • 기능의 계층적 확장: 인터페이스 상속을 통해 상위 인터페이스의 기본 기능을 확장하면서도, 구체적인 기능을 추가하여 더 세분화된 인터페이스를 만들 수 있음.
  • 코드의 유연성과 재사용성: 인터페이스 상속을 사용하여 기능을 단계적으로 구현함으로써, 다른 클래스에서 상속받은 인터페이를 재사용하고 유연하게 확장할 수 있음.
  • 다중 상속의 이점 활용: 인터페이스 간의 다중 상속이 가능하므로, 다양한 기능을 혼합하여 인터페이스를 구성할 수 있음.
//인터페이스 상속을 활용한 예제
public static void ApplyTransformation(ITransformable transformable)
{
    transformable.Move(10);
    transformable.Rotate(90);
    transformable.Scale(1.5);
}

TransformableObject obj = new TransformableObject();
ApplyTransformation(obj);

// 출력:
// 객체가 10만큼 이동했습니다.
// 객체가 90도 만큼 회전했습니다.
// 객체가 1.5배로 확대/축소되었습니다.

 

 

추상 클래스(abstract Class): 구체적인 구현 없이 일부 기능만 정의하고, 이를 상속받는 클래스가 구체적인 기능을 구현하도록 요구하는 클래스. 직접 객체를 생성할 수 없으며 상속을 통해서만 사용할 수 있음. 추상 클래스는 일부 메소드는 구현하고, 일부는 자식 클래스에서 반드시 구현하도록 강제할 때 사용.

 

특징

  • 인스턴스 생성 불가: 추상 클래스 자체로는 객체를 생성할 수 없음. 상속받은 자식 클래스에서만 인스턴스를 생성할 수 있음.
  • 추상 메소드와 일반 메소드 포함: 추상 클래스는 추상 메소드(구현이 없는 메소드)와 일반 메소드(구현이 있는 메소드)를 모두 가질 수 있음.
  • 상속을 통해 기능 강제: 추상 클래스에 선언된 추상 메소드는 자식 클래스에서 반드시 구현해야 함. 자식 클래스는 추상 메소드를 구첵적으로 정의해야 하며, 이를 통해 상속 구조에서 일관된 인터페이스를 제공할 수 있음.
  • 공통 코드 재사용: 자식 클래스에서 공통으로 사용할 기능을 추상 클래스에 구현함으로써, 코드의 중복을 줄이고 재사용성을 높일 수 있음.
public abstract class Animal
{
    // 추상 메서드: 구체적인 구현 없이 자식 클래스에서 구현을 강제
    public abstract void Speak();

    // 일반 메서드: 공통 기능을 구현할 수 있다.
    public void Eat()
    {
        Console.WriteLine("동물이 음식을 먹습니다.");
    }
}
//추상 클래스 상속 및 구현 예시
public class Dog : Animal
{
    // 추상 메서드 구현
    public override void Speak()
    {
        Console.WriteLine("강아지가 멍멍 짖습니다.");
    }
}

public class Cat : Animal
{
    // 추상 메서드 구현
    public override void Speak()
    {
        Console.WriteLine("고양이가 야옹 울습니다.");
    }
}
//사용예시
Animal dog = new Dog();
Animal cat = new Cat();

dog.Speak(); // 출력: 강아지가 멍멍 짖습니다.
dog.Eat();   // 출력: 동물이 음식을 먹습니다.

cat.Speak(); // 출력: 고양이가 야옹 울습니다.
cat.Eat();   // 출력: 동물이 음식을 먹습니다.

 

장점

  • 공통 기능 제공: 공통 기능을 추상 클래스에서 정의하여 코드의 중복을 줄이고, 자식 클래스에서 공통 로직을 재사용할 수 있음.
  • 구현 강제: 추상 메소드를 통해 자식 클래스에 특정 기능을 반드시 구현하도록 강제할 수 있음.
  • 우연한 설계: 일부 기능은 구현하고, 일부는 추상 메소드로 남겨 자식 클래스에서만 구현하도록 할 수 있음.

 

추상 클래스와 인터페이스의 차이점

특성 추상 클래스 인터페이스
메소드 구현 일부 메소드를 구현할 수 있음. 모든 멤버는 기본적으로 구현 없음.
다중 상속 다중 상속 불가 다중 상속 가능
사용 목적 상속받는 클래스의 공통 기능 제공 계약(contract)역할로 일관된 인터페이스 제공
필드 포함 가능 필드 선언 가능 필드 선언 불가

'2024-2 > C#' 카테고리의 다른 글

08.배열과 컬렉션, 인덱서  (0) 2024.11.10
07.프로퍼티  (0) 2024.11.10
05.클래스  (2) 2024.11.09
04.메소드  (0) 2024.11.08
03.연산자  (0) 2024.11.08

 

객체지향 프로그래밍(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("연료가 채워졌다.");
    }
}

 

객체지향의 특징

  1. 캡슐화: 데이터(속성)와 기능을 하나의 단위로 묶어 외부에서 접근할 수 없도록 보호하는 개념. 데이터 무결성 유지.
  2. 상속: 기존 클래스의 속성과 기능을 그대로 물려받고, 필요한 부분만 추가하거나 수정하여 새로운 클래스를 생성하는 것.
  3. 다형성: 같은 이름의 메소드가 다양한 형태로 동작할 수 있게 만드는 것.
  4. 추상화: 복잡한 시스템을 간단하게 표현하는 방법, 핵심적인 속성과 기능만 노출하여 사용하기 쉽게 함.

 

클래스(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;
    }
}

 

특징

  1. 기본 클래스의 멤버를 상속: 파생 클래스는 기본 클래스의 필드, 속성, 메소드를 자동으로 상속받아 사용할 수 있음.
  2. 기능 확장: 파생 클래스는 기본 클래스에 없는 새로운 멤버(속성이나 메소드)를 추가할 수 있음.
  3. 오버라이딩(overriding): 파생 클래스에서 기본 클래스의 메소드를 재정의 할 수 있음. 이때 virtualoverride 키워드를 사용하여 기본 클래스의 메소드를 파생 클래스에서 새롭게 구현할 수 있음.
//메소드 오버라이딩
public class Car
{
    public virtual void Drive()
    {
        Console.WriteLine("차량이 주행 중입니다.");
    }
}

public class SportsCar : Car
{
    public override void Drive()
    {
        Console.WriteLine("스포츠카가 고속으로 주행 중입니다!");
    }
}

 

상속의 장점과 제약 사항

  • 코드 재사용: 기본 클래스의 멤버를 재사용함으로써 중복 코드를 줄이고 유지보수성을 높일 수 있음.
  • 확장성: 기본 클래스의 기능을 파생 클래스에서 확장하여 새로운 기능을 추가하거나 기존 기능을 변경할 수 있음.
  • 유연성: 다형성을 통해 기본 클래스와 파생 클래스를 같은 타입으로 다룰 수 있어 객체 간의 상호작용을 유연하게 설계할 수 있음.
  • C#에서는 다중 상속(하나의 클래스가 여러 기본 클래스를 상속받는 것)을 지원하지 않으며, 한 클래스는 오직 하나의 기본 클래스만 상속받을 수 있음.
  • sealed 키워드를 사용하여 특정 클래스의 상속 금지 가능.

 

base 키워드: 파생 클래스에서 기본 클래스의 멤버(필드, 메소드, 생성자 등)에 접근할 때 사용되는 키워드. 기본 클래스의 생성자를 호출하거나 재정의된 메소드를 호출할 때 사용.

 

주요 용도

  1. 기본 클래스의 생성자 호출: 파생 클래스의 생성자에서 base를 사용하여 기본 클래스의 생성자 호출 가능.
  2. 기본 클래스의 메소드나 속성 접근: 파생 클래스에서 기본 클래스의 메소드를 재정의(오버라이딩)한 경우, 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("스포츠카가 고속으로 주행 중입니다!");
    }
}

 

 

형식 변환: 특정 데이터 형식을 다른 형식으로 변환하는 것. 이 과정에서 isas 키워드가 자주 사용됨. 이러한 키워드는 객체의 타입을 확인하고, 안전하게 형변환을 수행하는 유용함.

  • 암시적 변환(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로 선언되어 있으며, DogCat 클래스는 각각 이 메소드를 override하여 재정의 함. 따라서 DogCat 객체는 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 메소드 호출.

오버라이딩과의 차이점

  • 오버라이딩은 virtualoverride를 사용하여 런타입 다형성을 구현하며, 부모 클래스의 메소드를 재정의 하는 방식.
  • 메소드 숨기기는 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);
    }
}

-InnerClassOuterClass 내부에 정의된 중첩 클래스. OuterClassShowMessage 메소드는 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.csPerson2.cs 파일에 나누어 정의되었지만, 컴파일후 하나의 클래스로 결합되어 모든 멤버에 접근할 수 있음.

 

장점

  1. 코드 관리의 편리성: 클래스가 복잡하고 길어질 때, 기능별로 분리하여 코드를 유지보수하기 쉬움.
  2. 여러 개발자의 협업 용이: 하나의 클래스를 여러 파일로 나눠 작업할 수 있어, 여러 개발자가 동시에 작업할 때 충돌을 줄일 수 있음.
  3. 자동 생성 코드와 사용자 정의 코드의 분리: 코드 생성 도구가 자동 생성하는 코드와 사용자 정의 코드를 별도 파일로 분리하여, 코드 생성 도구가 파일을 덮어쓸 때 사용자 정의 코드가 영향을 받지 않도록 할 수 있음.

주의 사항

  • 모든 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}");
    }
}

 

장점

  1. 기존 클래스 수정 없이 기능 확장: 기존 클래스를 변경하지 않고도 메소드를 추가할 수 있어, 외부 라이브러리나 기본 클래스의 기능을 확장할 때 유용함.
  2. 직관적인 사용: 확장 메소드를 통해 기존 인스턴스 메소드처럼 메소드를 호출할 수 있어, 코드가 더 직관적이고 읽기 쉬움.
  3. 코드 유지보수성 향상: 자주 사용하는 기능을 확장 메소드로 정의하면, 중복 코드를 줄이고 유지보수가 용이해짐.

주의 사항

  • 확장 메소드는 static 클래스 안에 정의 되어야 함.
  • 확장 메소드의 우선순위는 인스턴스 메소드보다 낮음. 동일한 이름의 인스턴스 메소드가 있으면, 확장 메소드가 아닌 인스턴스 메소드가 호출됨.
  • 남용할 경우 코드가 복잡해질 수 있으므로, 실제 필요한 경우에만 사용해야 함.

 

구조체(Struct): C#에서 값 타입(Value Type)을 정의하기 위한 데이터 구조. 클래스와 유사하게 필드, 메소드, 프로퍼티, 생성자 등을 가질 수 있지만, 주로 작고 단순한 데이터를 표현할 때 사용함.

 

특징

  1. 값 타입: 구조체는 값 타입으로, 힙이 아닌 스택에 저장되며 객체를 복사할 때 값이 복사됨. (클래스는 참조 타입)
  2. 상속 불가: 구조체는 클래스를 상속하거나 상속받을 수 없음. 단, 인터페이스는 구현할 수 있음.
  3. 기본 생성자 없음: 구조체는 매개변수가 없는 생성자를 정의할 수 없음. 컴파일러가 자동으로 기본 생성자를 제공하며, 모든 필드는 기본값으로 초기화됨.
  4. 불변성(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 구조체는 XY 좌표 값을 저장하며, 거리 계산 메소드 DistanceTo를 정의함. 생성자를 통해 초기화되며, XY는 읽기 전용 프로퍼티로 설정되어 불변성을 유지.

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 구조체는 불변 구조체로, XY 필드는 읽기 전용 프로퍼티로 설정되었으며, 초기화 이후 변경할 수 없음. 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): 여러 개의 값을 하나의 그룹으로 묶어 저장할 수 있는 데이터 구조. 반환값이 하나뿐인 메소드에서도 여러 개의 값을 반환할 수 있으며, 여러 데이터 항목을 간단하게 저장하고 사용할 수 있음.

 

특징

  1. 다수의 값을 한 번에 저장: 여러 타입의 값을 하나로 묶어 저장 가능.
  2. 익명 필드: 기본적으로 필드 이름 없이 인덱스로 접근하거나, 필요한 경우 필드 이름을 지정할 수 있음.
  3. 읽기 전용: C#의 튜플은 불변. 생성된 후 튜플 항목의 값을 변경할 수 없음.
  4. 간단한 사용: 데이터를 빠르게 묶어서 전달하거나 반환할 때 편리하게 사용할 수 있음.
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).

 

장점

  • 코드 가독성 향상: 복잡한 조건 검사를 패턴을 이용해 간결하고 직관적으로 표현할 수 있음.
  • 구조화된 데이터 검사에 용이: 튜플과 레코드처럼 구조화된 데이터의 개별 요소를 쉽게 검사할 수 있음.
  • 유연한 조건 검사: 와일드 카드_와 함께 사용하여 특정 위치만 검사하거나, 특정 요소는 무시하는 등 유연하게 조건을 설정할 수 있음.

'2024-2 > C#' 카테고리의 다른 글

07.프로퍼티  (0) 2024.11.10
06.인터페이스와 추상 클래스  (1) 2024.11.09
04.메소드  (0) 2024.11.08
03.연산자  (0) 2024.11.08
02. 데이터 보관하기  (0) 2024.11.08

+ Recent posts