본문 바로가기

내일배움캠프/C#문법종합반

[TIL 24.04.29~05.05] 4주차 강의 필기노트

이번 강의는 아침마다 정규 수업시간이 시작하기 전후로 틈틈이 들어서 하루만에 끝나지 못했다. 오래 자리를 잡고 들은 것이 아니라 한 강의조차도 쪼개서 듣고 그만큼에 대한 내용을 숙지했다고 생각한 다음에야 그 다음 내용을 들어왔다.

목   차

     


    인터페이스와 열거형

    다중 상속을 사용하지 않는 이유

    1. 다이아몬트 문제
    2. 설계의 복잡성 증가
    3. 이름 충돌과 해결의 어려움
    4. 설계의 일관성과 단순성 유지

    인터페이스를 사용하는 이유

    1. 코드의 재사용성
    2. 다중 상속 제공
    3. 유연한 설계

    인터페이스(Interface)

    interface IMyInterface
    {
        void Method1();
        int Method2(string str);
    }
    
    class MyClass : IMyInterface
    {
        public void Method1()
        {
            // 구현
        }
    
        public int Method2(string str)
        {
            // 구현
            return 0;
        }
    }

      다음이 바로 인터페이스 와 그를 상속한 클래스이다. 이때, 만약 MyClass에서 인터페이스의 맴버들의 구현부를 작성해주지 않는다면 에러 메시지가 뜨게 된다. 여기서 우리는 인터페이스가 하는 일이 추상클래스와 다르다는 것을 알 수 있다. 추상클래스는 그 아래에 여러 종류의 메서드(virtual, abstract, 또는 일반 메서드 등)를 포함한다. 하지만, 인터페이스는 기본적으로 모든 메서드가 abstract이란 수식 키워드만 없는 추상 메서드라고 볼 수 있다. 

     

     C# 8.0 이후부터는 인터페이스에도 구현부를 넣을 수는 있게 되었다. 하지만, 여전히 정석적인 방법은 추상 메서드처럼 구현은 자식 클래스로 미루되, 반드시 자식 클래스가 그 메서드의 구현부를 작성하도록 하는 것이다. 굳이 추상클래스 대신 인터페이스를 사용하는 건 아마 비슷한 기능을 위한 함수들끼리의 묶음을 만들기 위해서가 아닐까 싶다.

    #(+) Java에서의 상속 키워드

    Java에서 상속을 할때는 : 대신 키워드를 사용해야 상속을 받을 수 있는데, 이때 클래스를 상속받을 때는 extends를 쓰는 반면, 인터페이스는 implements 를 썼었다. extend는 확장하다, implement는 실행하다 라는 뜻이다. 명사로는 도구, 기기 라는 뜻으로도 쓰인다.

    (+) 인터페이스의 구현

    다음은 인터페이스에 구현부를 넣는 사례들이다.

    #기존 인터페이스 확장

    public interface ILogger
    {
        void Log(string message);
        // 새로운 기능으로 로그 레벨을 추가하지만, 기존 구현에는 영향을 주지 않습니다.
        void LogWithLevel(string message, string level)
        {
            Log($"{level}: {message}");
        }
    }
    
    // ConsoleLogger는 LogWithLevel을 구현하지 않아도 기본 구현을 사용할 수 있습니다.
    public class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

     이렇게 인터페이스의 구현부도 자식클래스에서 불러올 수 있게 되니 여기까지 알게 되었을 때는 추상클래스와 그럼 대체 뭐가 달라서 인터페이스만 다중 상속이 가능한지 의문이 생겼었다.

     

    #선택적 메서드 구현

    public interface IAnimal
    {
        void Eat();
        void Sleep();
        // 기본 구현을 통해 Hunt 메서드는 선택적으로 재정의할 수 있습니다.
        void Hunt()
        {
            Console.WriteLine("Hunting is not applicable.");
        }
    }
    
    // Lion은 Hunt를 재정의하지만, 다른 동물은 그럴 필요가 없을 수 있습니다.
    public class Lion : IAnimal
    {
        public void Eat()
        {
            Console.WriteLine("Lion is eating.");
        }
    
        public void Sleep()
        {
            Console.WriteLine("Lion is sleeping.");
        }
    
        public void Hunt()
        {
            Console.WriteLine("Lion is hunting.");
        }
    }

     이처럼, 인터페이스에 구현이 되어 있다면, 인터페이스의 멤버 메서드임에도 abstract가 아닌 virtual 함수가 된 것처럼 보인다. 하지만 이 함수는 정확히는 default implementation이라고 한다. 즉, 기본적으로는 이 형식으로 자식 클래스에서 "이행"하게 하는 것이고, 필요하다면 새로 정의해서 쓰게 한다는 거다.override 키워드를 쓰지 않을 뿐. override와 같은 메커니즘을 가지지만, implementing 또는 reimplementing이라고 부른다고 한다. 

     

    #다중 상속 비슷한 효과

    public interface IWalkable
    {
        void Walk()
        {
            Console.WriteLine("Walking...");
        }
    }
    
    public interface ISwimmable
    {
        void Swim()
        {
            Console.WriteLine("Swimming...");
        }
    }
    
    // Duck는 Walk와 Swim의 기본 구현을 모두 사용합니다.
    public class Duck : IWalkable, ISwimmable
    {
        // Duck 클래스는 두 인터페이스의 기능을 조합하여 사용할 수 있습니다.
    }

     이 예시를 통해 클래스는 다중상속을 하지 않는데 인터페이스는 가능한건지 확실히 알 수 있었다. 클래스에서 다중상속을 금지하는 가장 큰 이유는 겹치는 메서드를 서로 다른 부모를 통해 동시에 상속받을까가 있었다. 하지만, 인터페이스로 같은 기능을 하는 메서드를 받는다면 그건 인터페이스 기획의 문제가 된다. 애초에 인터페이스 자체가 기능 단위로 묶은 것이기 때문이다. 

     

     이 말은 즉, 클래스와 인터페이스가 하는 기능 자체는 비슷해 보이지만, 클래스가 멤버들을 묶는 기준과 인터페이스의 기준이 서로 달라 인터페이스는 다중 상속을 해도 문제가 덜 생긴다고 볼 수 있다.

     

    #유지보수성 향상

    코드 상의 새로운 점은 없어 이 부분은 말로 설명한다. 구현부를 상위 클래스에서 작성할 수 있다면, 특정 메서드가 여러 자식 클래스에 공통적으로 필요할 때 이 구현 메서드 하나만 선언하고, 마찬가지로 그 메서드를 수정할 필요가 있을 때도 자식 클래스 일일이 찾아다닐 필요 없이 그냥 부모 인터페이스 하나만 수정하면 다 자동으로 수정사항이 적용되기 때문에 유지보수에 용이하다.

    예외 처리 및 값형과 참조형

    예외 처리 개요

    #예외의 정의

    예외: 프로그램 실행 중에 발생하는 예기치 않은 상황. 

      ex) for 문 안에서 배열에 index를 하나씩 넣는 와중 배열의 크기보다 큰 i로 array[i]의 값을 불러오려 할 때.

     

    # 예외 처리의 필요성 & 장점

    • 프로그램 안정적으로 유지
    • 예외 처리를 통해 오류 상황을 적절히 처리하고 프로그램의 실행을 계속할 수 있음
    •  디버깅이 용이함

    #예외 처리 구현

    try
    {
        // 예외가 발생할 수 있는 코드
    }
    catch (ExceptionType1 ex)
    {
        // ExceptionType1에 해당하는 예외 처리
    }
    catch (ExceptionType2 ex)
    {
        // ExceptionType2에 해당하는 예외 처리
    }
    finally
    {
        // 예외 발생 여부와 상관없이 항상 실행되는 코드
    }

    #catch 블록의 우선순위

    위에서부터 순서대로 실행된다. 

    위의 코드를 예로 들면, ExceptionType1이 먼저 처리되고 그 후에 ExceptionType2가 처리되는 것이다.

    다만, 상속 관계에 있는 경우 상위 예외타입의 catch 블록이 먼저 실행된다.

    #다중 catch 블록

    catch는 역시나 위의 코드와 같이 여러 번 실행될 수 있다.

    #예외 객체

    예외의 종류를 일컫는 객체. 대충 exception으로 끝나는 것들.

    너무 많아서 이건 항상 찾아보면서 한다... 자주 쓰는 건 그나마 index 계열?

    finally 블록

    개요부문의 예제코드 가장 아래에 finally 블록이 있는 것을 확인할 수 있다. 

    위의 설명처럼 예외 발생여부와 상관없이 항상 실행되고, 예외 발생시 정리 작업이나 리소스 해제 등의 코드를 포함한다.

    생략 역시 가능하다.

    사용자 정의 예외

    내가 필요할 때 Exception 클래스를 상속받아 만드는 예외 클래스를 말한다.

    이를 통해 미리 정의되어 있지 않은 예외도 try-catch 문을 통해 거를 수 있다.

    값과 참조형

    #값형(Value Type)

    변수에 값을 직접 저장하는 것.

    int, float, double, bool, string 등의 기본 데이터 타입들은 모두 값형에 해당함.

     

    #참조형(Reference Type)

    데이터의 주소를 저장하는 것. 

         => a에 b의 주소를 저장하고 a의 값을 수정하면 b의 값도 함께 수정된다.

    배열, 클래스, 인터페이스 등이 참조형에 해당함.

     

    #값형 vs 참조형

    값형: "데이터"를 변수에 저장함.

    참조형: "참조"를 변수에 저장함.

     

    값형 복사(깊은 복사): "데이터"를 "새로운 변수"에 저장함.

    참조형 복사(얕은 복사): "데이터"가 저장된 "위치"를 "새로운 변수"에 저장함. 

     

    따라서 참조형은 두 개의 변수가 하나의 데이터를 가리키게 하고, 둘 중 하나라도 수정되면 다른 하나 역시 함께 수정되기 하는 것이다.

    박싱과 언박싱

    박싱: 값 -> 참조형

    값형 변수의 값을 힙 영역에 할당된 객체로 래핑.

    참조형 변수로 다뤄질 수 있음.

    메모리 오버헤드 주의.

     

    언박싱: 박싱된 객체 -> 값

    객체에서 값을 추출하여 값형 변수에 할당.

    명시적 타입캐스팅 필요, 런타임에서 타입 검사가 이루어짐.

    잘못 언박싱하면 런타임 에러가 나니 주의.

     

    둘 모두 메모리 관리에 유의해야 한다. 가비지 컬렉션의 대상이 될 수 있으므로.

    박싱된 값과 원래의 값형은 서로 독립적! -> 상호 영향이 없음.

     

    박스 언박스 하니 어려워 보이는데 그냥 배열/리스트 생각하면 편함.

    List<object> myList = new List<object>();
    
    // 박싱: 값 형식을 참조 형식으로 변환하여 리스트에 추가
    int intValue = 10;
    myList.Add(intValue); // int를 object로 박싱하여 추가
    
    float floatValue = 3.14f;
    myList.Add(floatValue); // float를 object로 박싱하여 추가
    
    // 언박싱: 참조 형식을 값 형식으로 변환하여 사용
    int value1 = (int)myList[0]; // object를 int로 언박싱
    float value2 = (float)myList[1]; // object를 float로 언박싱

     

    델리게이트, 람다 및 LINQ

    델리게이트 (Delegate)

    #Delegate란?

    함수 포인터: 메서드를 참조하는 타입!

    여러 메서드를 줄줄이 연결해 사용하는 것도? 가능!

    메서드를 매개변수로 전달하거나 변수에 할당할 수 있음.

    #예시1: 하나 이상의 메서드를 Delegate에 등록하기

    delegate void MyDelegate(string message);
    
    static void Method1(string message)
    {
        Console.WriteLine("Method1: " + message);
    }
    
    static void Method2(string message)
    {
        Console.WriteLine("Method2: " + message);
    }
    
    class Program
    {
        static void Main()
        {
            // 델리게이트 인스턴스 생성 및 메서드 등록
            MyDelegate myDelegate = Method1;
            myDelegate += Method2;
    
            // 델리게이트 호출
            myDelegate("Hello!");
    
            Console.ReadKey();
        }
    }

    #예시2: event 사용하기

    delegate void MyDelegate(string message);
    
    static void Method1(string message)
    {
        Console.WriteLine("Method1: " + message);
    }
    
    static void Method2(string message)
    {
        Console.WriteLine("Method2: " + message);
    }
    
    class Program
    {
        static void Main()
        {
            // 델리게이트 인스턴스 생성 및 메서드 등록
            MyDelegate myDelegate = Method1;
            myDelegate += Method2;
    
            // 델리게이트 호출
            myDelegate("Hello!");
    
            Console.ReadKey();
        }
    }

    람다 (Lambda)

    #Lambda란? 

    익명 메서드를 만드는 방법.

    델리게이트를 사용하여 변수에 할당하거나, 메서드의 매개변수로 전달 가능.

    #Lambda의 구현

    //형식
    (parameter_list) => expression
    
    //ex) 기본
    Calculate calc = (x, y) => { return x + y; }; 
    
    //ex2) 더 간결하게: 표현식 1개일 때
    Calculate calc = (x, y) => x + y;

    #사용 예제

    // 델리게이트 선언
    delegate void MyDelegate(string message);
    
    class Program
    {
        static void Main()
        {
            // 델리게이트 인스턴스 생성 및 람다식 할당
            MyDelegate myDelegate = (message) => //+=도 가능
            {
                Console.WriteLine("람다식을 통해 전달된 메시지: " + message);
            };
    
            // 델리게이트 호출
            myDelegate("안녕하세요!");
    
            Console.ReadKey();
        }
    }

    Func과 Action

    #/Func, Action이란?

    Func과 Action: 델리게이트를 대체하는 미리 정의된 제네릭 형식.

    #Func

    Func: 값을 반환하는 메서드를 나타내는 델리게이트.

    // Func를 사용하여 두 개의 정수를 더하는 메서드
    int Add(int x, int y)
    {
        return x + y;
    }
    
    // Func를 이용한 메서드 호출
    Func<int, int, int> addFunc = Add; //int1, 2: 인자, int3: 반환자
    int result = addFunc(3, 5);
    Console.WriteLine("결과: " + result);​

    #Action

    Action: 값을 반환하지 않는 메서드를 나타내는 델리게이트.

    // Action을 사용하여 문자열을 출력하는 메서드
    void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }
    
    // Action을 이용한 메서드 호출
    Action<string> printAction = PrintMessage;
    printAction("Hello, World!");

    LINQ (Language INtegrated Query)

    기본 구조:

    var result = from 변수 in 데이터소스
                 [where 조건식]
                 [orderby 정렬식 [, 정렬식...]]
                 [select 식];
    • var 키워드는 결과 값의 자료형을 자동으로 추론합니다.
    • from 절에서는 데이터 소스를 지정합니다.
    • where 절은 선택적으로 사용하며, 조건식을 지정하여 데이터를 필터링합니다.
    • orderby 절은 선택적으로 사용하며, 정렬 방식을 지정합니다.
    • select 절은 선택적으로 사용하며, 조회할 데이터를 지정합니다.

     근데 이건 약간 커스텀이고 다양한 확장메서드 역시 많이 있다.

    var filtered = collection.Where(x => x.Age > 18); //필터링
    
    var names = people.Select(p => p.Name); //프로젝션
    
    var sorted = collection.OrderBy(x => x.Name); //정렬
    
    var grouped = collection.GroupBy(x => x.Department); //그룹화
    
    var count = collection.Count(x => x.Age > 18); //요소의 수
    
    var totalAge = people.Sum(p => p.Age); //요소의 합
    
    var oldest = people.Max(p => p.Age); //최대 - 최소는 Min
    
    var firstPerson = people.FirstOrDefault(p => p.Age > 18); //첫 요소 선택
    
    var person = people.Single(p => p.ID == 1); //하나의 요소 선택(둘 이상이 대상이거나 대상 없으면 에러.)
    
    var allAdults = people.All(p => p.Age >= 18); //모든 요소가 조건을 만족하는지
    
    var anyAdults = people.Any(p => p.Age >= 18); //하나 이상이 조건을 만족하는지
    
    var distinctAges = people.Select(p => p.Age).Distinct(); //중복 제거

    고급 자료형 및 기능

    Nullable 형

    #Nullable이란?

    C#에서 null 값을 가질 수 있는 값형에 대한 특별한 형식.

    • 기본적으로 값형은 null값을 허용하지 않으나 Nullable로 선언하면 가능해짐.
    int? nullableInt; //<1>
    int nonNullableInt = nullableInt ?? 0; //<2>

    <1>: Nullable<int> 형식의 선언. 기본값은 null.

    <2>: nonNullableInt는 nullableInt가 null이 아닐때 할당되고, null이면 0이 할당됨.

    #번외: C++에도 존재했던 개념인가?

    C++로 int a; 를 선언했을 때와 C#의 int a; 로 선언했을 때의 초기값은 다르다. C#은 자동으로 a에 기본값인 0을 넣어주는 반면, C++는 그런거 없다. 아마 그런 이유로 자동 초기화를 막기 위한 방법 중 하나로 nullable이 생기지 않았나 생각이 든다.

    C++에서 별도의 초기화 없이 a를 출력한다면? >> 메모리에 있는 임의의 값 출력

    C#에서 nullable을 통해 null값을 가진 a를 출력한다면?  >> 아무것도 출력되지 않음.

    문자열 빌더(String Builder)

    #StringBuilder란?

    1. 문자열 조작: 다양한 메서드를 통한 문자열 추가/삽입/치환/삭제작업 가능.
    2. 가변성
    3. 효율적인 메모리 관리

    string과의 차이점: 가변성의 유무. 

        string이 문자열 크기를 바꿀 수 있는 것처럼 보여도 사실은 새로운 공간을 할당받아 다시 적는 메커니즘을 가지기에, 자주 수정이 되어야 할 문자열은 stringbuilder를 쓰는게 좋다.

    #주요 메서드

    • Append: 기존의 문자열 끝에 새로운 내용 추가.
      • AppendLine이라는 친구도 있는데, 문자열+줄바꿈의 형태로 추가됨.
    • Insert: 지정된 인덱스 위치에 문자열 삽입. Insert(index, string);
    • Remove: index위치의 문자열 N개를 제거. Remove(index, N);
    • Replace: 특정 문자 또는 문자열을 모두 다른 문자 또는 문자열로 교체. Replace('a', 'b');
    • Clear: 객체 안의 모든 문자를 지움.
    • ToString: StringBuilder의 값을 String으로 바꿈.
    • Length: 현재 저장된 문자열의 길이를 가져오거나 설정함.
    • Capacity: 현재 할당된 용량을 가져오거나 설정.

    #번외: Java에도 있었다!

    끝이다. 그냥 기억이 나서 적어봤다...


     

    마무리

    분명 다 썼는데... 날아갔다... Tistory  수정상태로 너무 오래 방치하면 자동 로그아웃된다는 사실을 나는 몰랐고... 그때 저장되지 않은 정보는 날아가고... 너무 슬프다. 만약 빈 슬롯이 있다면 열심히 고치고 있다 생각해주세요...ㅠㅅㅠ