이번 강의는 아침마다 정규 수업시간이 시작하기 전후로 틈틈이 들어서 하루만에 끝나지 못했다. 오래 자리를 잡고 들은 것이 아니라 한 강의조차도 쪼개서 듣고 그만큼에 대한 내용을 숙지했다고 생각한 다음에야 그 다음 내용을 들어왔다.
목 차
인터페이스와 열거형
다중 상속을 사용하지 않는 이유
- 다이아몬트 문제
- 설계의 복잡성 증가
- 이름 충돌과 해결의 어려움
- 설계의 일관성과 단순성 유지
인터페이스를 사용하는 이유
- 코드의 재사용성
- 다중 상속 제공
- 유연한 설계
인터페이스(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란?
- 문자열 조작: 다양한 메서드를 통한 문자열 추가/삽입/치환/삭제작업 가능.
- 가변성
- 효율적인 메모리 관리
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 수정상태로 너무 오래 방치하면 자동 로그아웃된다는 사실을 나는 몰랐고... 그때 저장되지 않은 정보는 날아가고... 너무 슬프다. 만약 빈 슬롯이 있다면 열심히 고치고 있다 생각해주세요...ㅠㅅㅠ
'내일배움캠프 > C#문법종합반' 카테고리의 다른 글
[TIL 24.05.05~06] 5주차 강의-알고리즘 기초, 정렬 알고리즘 (0) | 2024.05.05 |
---|---|
[TIL 24.04.28] 3주차 강의 필기노트 (0) | 2024.04.28 |
[TIL 24.04.23] 3주차 과제1: Snake Game (0) | 2024.04.23 |
[TIL 24.04.23] 2주차 과제2: Tic Tac Toe (0) | 2024.04.23 |
[TIL 24.04.22] 1주차~2주차 강의 필기노트 (0) | 2024.04.22 |