
[문과 코린이의 IT 기록장] C# - 클래스 2 (구조체 , 제네릭 (Generic), Delegate / Event)
C# 프로그래밍 기초 - 인프런 | 강의
본 강좌는 C# 문법 위주로 구성되어있지 않습니다. 클래스를 이해하고 만드는 요령 위주로 구성되어 있습니다. 기초 문법도 다루지만 많은 예제를 가지고 진행하기 때문에 프로그램 실전 작성
www.inflearn.com
1. 구조체 (Struct)
1) 참조 형식(클래스)과 값 형식(구조체)에 대한 이해
- Heap Memory / Stack Memory


using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Program { static void Main(string[] args) { // Value Type의 특성 // - 동작방법 /*int a = 1; int b = 2; Console.WriteLine($"1. a = {a} b = {b}"); int c = ValueTypeTest(a, b); Console.WriteLine($"2. a = {a} b = {b}");*/ Console.WriteLine("클래스 : 참조 형식"); Student1 st1 = new Student1(); st1.Name = "Lee"; Console.WriteLine(st1.Name); ReferenceTypeTest(st1); Console.WriteLine(st1.Name); Console.WriteLine("-----------------------------------"); Console.WriteLine("구조체 : 값 형식"); Student2 st2 = new Student2(); st2.Name = "Lee"; Console.WriteLine(st2.Name); ReferenceTypeTest(st2); Console.WriteLine(st2.Name); } static int ValueTypeTest(int a, int b) // 호출할 때 값을, copy해 준 것. (참조가 X) { Console.WriteLine($"3. a = {a} b = {b}"); a++; b++; Console.WriteLine($"4. a = {a} b = {b}"); return a + b; } static void ReferenceTypeTest(Student1 a) { a.Name = "Kim"; } static void ReferenceTypeTest(Student2 a) { a.Name = "Kim"; } } /// <summary> /// 참조 형식으로 학생 클래스입니다. /// </summary> class Student1 { public string Name { get; set; } public int Age { get; set; } } struct Student2 { public string Name { get; set; } public int Age { get; set; } } }

2) 구조체 정의
- 클래스 정의와 유사하다.
- class 예약어를 struct 예약어로 대체한다.
3) 구조체가 클래스와 다른점
- 인스턴스 생성을 new로 해도 되고, 안 해도 된다.
- 참조 형식이 아니라 값 형식이다.
- 기본 생성자는 명시적으로 정의 불가능하다.
- 매개변수를 갖는 생성자를 정의해도, 기본 생성자가 C# 컴파일러에 의해 자동 포함된다.
- 매개변수를 받는 생성자의 경우, 반드시 해당 코드 내에서, 구조체의 모든 필드에 값을 할당해야 한다.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Program { static void Main(string[] args) { Student st = new Student(); Student st1 = st; // 메모리 자체가 통째로 복사되는, 깊은 복사가 이루어짐. Student st2; // new를 쓰지 않고 생성 가능 (값 형식 변수이기 때문) st.Name = "Lee"; st1.Name = "Kim"; //st2.Name = "Kim"; Console.WriteLine(st.Name); Console.WriteLine(st1.Name); // 깊은 복사이기 때문에, 새로운 메모리 공간에 st1이 만들어진 것. 따라서, 둘은 다른 값을 가짐 AAA(st); } static void AAA(Student st) // 깊은 복사 발생. (스택에서 발생) - 구조체는 참조형식 X. (매개변수만 참조형식으로 힙에 존재) { } /// <summary> /// 참조 형식으로 학생 클래스입니다. /// </summary> struct Student { public string Name { get; set; } public int Age { get; set; } // 1. Sturct에 대해, C컴파일러가 알아서, 매개변수가 없는 생성자를 만들어준다. // public Student() { }; // 구조체는 명시적으로 기본 생성사를 정의하는 것이 불가능하다. // 2. 매개변수를 갖는 생성자의 경우, 반드시 해당 코드 내에 구조체의 모든 필드의 값을 할당해야 한다. public Student(string name, int age) { Name = name; Age = age; } } } }
2. 제네릭 (Generic)
- OOP에서 Object를 처리하는데 발생하는 문제점들을 개선하기 위한 하나의 방법
1) Generic 탄생 동기
- ArrayList를 사용할 때, boxing, unboxing의 문제와, 코드 중복의 문제를 해결하기 위해, Generic이라는 개념이 나타났다.
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Student; namespace class_0804 { class generic { public void Run() { ArrayList list = new ArrayList(); list.Add(4); // boxing 발생 int a = (int)list[0]; // unboxing 발생 PrintMe(3); PrintMe("String"); } // [문제] 여러 종류의 Type이 존재할 때, 이렇게 함수를 다 나눠서 코드를 작성해야 할 것인가? // - 코드 중복이 굉장히 많이 발생 private void PrintMe(int val) { Console.WriteLine($"val = {val}"); } private void PrintMe(string val) { Console.WriteLine($"val = {val}"); } } }
2) Generic 사용법 : ArrayList -> List<T>
a. 프로젝트 1 - [ Generic.cs ]
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Student; namespace class_0804 { class generic { public void Run() { // [해결방법] List 컬랙션 사용 // 1. int 사용 // List<T> list = new List<T>(); // T : 타입 변수명 = Type Parametor (Generic) List<int> list = new List<int>(); // int ArrayList 생성 : 이 ArrayList에는 int만 들어온다. 다른 것이 들어오면 error. list.Add(1); // 가능 O // list.Add("string"); // 불가능 int a = list[0]; // (int)로 캐스팅 해줄 필요 없음. 즉, unboxing이 필요 없음. (Type을 int로 설정했기 때문) // 2. 클래스 사용 // 다른 프로젝트의 클래스를, 각각 클래스는 어셈블리 파일(.exe, .dll)로 구성되어 있으며, 이들 간에 서로가 보일려면 public이라는 접근 제한자를 쓸 수 밖에 없음. // 참조로, Student namespace를 가져온 후, StudentC 사용 List<StudentC> list1 = new List<StudentC>(); StudentC st = new StudentC(); // st 인스턴스 생성 list1.Add(st); //list1.Add(1); // 타입 불일치. 불가능. } } }
b. 프로젝트 2 - [StudentC.cs]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Student { // public class Student = internal class Student // ( internal이란? : 동일한 어셈블리 내에서는 public과 같이 동작됨. 그러나 다른 어셈블리에서 바라봤을 때는 보이지 않는다. ) // 다른 프로젝트에서 해당 클래스를 가져다 쓰기 위해서는, public을 넣어줘야 함. public class StudentC:IComparable // IComparable : 두 객체를 비교하기 위한 인터페이스 (배열 및 컬랙션을 정렬하는데 효과적인 인터페이스) { public string Name { get; set; } public int StudentSex { get; set; } public int Score { get; set; } public int CompareTo(object obj) { StudentC st1 = obj as StudentC; // return Score - st1.Score; return Name.CompareTo(st1.Name); } public override string ToString() { return $"{Name}[{StudentSex}][{Score}]"; } } }
3) Generic class & Generic method
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Student; namespace class_0804 { class generic { public void Run() { // 1. Generic Class < T > PrintAny<int> p1 = new PrintAny<int>(); // PrintAny형의 인스턴스 p를 생성 (내부 T는 모두 int형으로 변환) p1.Print(3); PrintAny<double> p2 = new PrintAny<double>(); // PrintAny 인스턴스 p2를 생성 (내부 T는 모두 double형으로 변환) p2.Print(3.141592); PrintAny<StudentC> p3 = new PrintAny<StudentC>(); StudentC st = new StudentC(); st.Name = "홍길동"; p3.Print(st); // 2. Generic Method PrintAny1 p4 = new PrintAny1(); p4.Print<int>(15); p4.Print(10); // <int>를 지정해주지 않아도, 타입 추정 가능. // 3. Generic Class < T, U > PrintAny2<int, string> p5 = new PrintAny2<int, string>(); p5.Print(33, "string"); } } // [문제] 수많은 기본 타입, class들이 존재함. 그런데, 이렇게 type별로 print해야하는 클래스를 모두 만드는 것은 어려움 class PrintInteger { public void Print(int val) { Console.WriteLine($"val = {val}"); } } // [해결방법] Generic Class<T>로 해결 class PrintAny<T> // Generic Class : T는 어떤 type이던, 컴파일 타임 때 지정된 type으로 전부 다 변경됨. { public void Print(T val) // T는 타입변수. 따라서 이렇게 선언 가능 { Console.WriteLine($"val = {val}"); // Console.WriteLine($"val = {val + 1}"); // 이는 불가능. 특정 타입에 관계된 연산 처리와 같은 것을 Generic이라고 쓸 수 없음. 즉, 범용으로 커버하는데 사용될 수 없음. } } // [해결방법] Generic Method로 해결 class PrintAny1 { public void Print<T>(T val) // Generic Method { Console.WriteLine($"val = {val}"); } } // Generic Class <T, U> class PrintAny2<T, U> { public void Print(T val1, U val2) { Console.WriteLine($"val1 = {val1}, val2 = {val2}"); } } // T와 U가 IComparable이라는 인터페이스를 구현하지 않은 타입이라면 받아들이지 못한다. ex. :IComparable을 하지 않은 클래스 class PrintAny3<T, U> where T:IComparable where U:IComparable { public void Print(T val1, U val2) { Console.WriteLine($"val1 = {val1}, val2 = {val2}"); } } }
3. Delegate / Event
1) Delegate
- Method를 값으로 갖는 타입
int a = 3; // 3이라는 value를 가지는 타입 - value 타입 Student st = new Student(); // Student 객체를 갖는 타입 - reference 타입 delegate void MyDelegate(int a); // 함수를 값으로 갖는 타입 - delegate 타입
- delegate가 붙으면 method가 아니라 MyDelegate가 Student, int 처럼 type으로 선언된다.
using Student; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Dele { // 2. delegate void MyDelegate(int val); // delegate type 맴버변수로 선언 // 3. delegate int MyDelegate1(int val); public void Run() { int a = 3; Console.WriteLine(a); StudentC st = new StudentC(); st.Name = "홍길동"; Console.WriteLine(st); // 1. 그냥 메소드로 실행 DelegateTest(3); // 정상적으로 호출됨 // [ C언어에서 함수 포인터와 개념이 유사 ] // 2. delegate 사용. // 클래스를 선언하듯이, MyDelegate타입을 쓸 수 있게 됨 MyDelegate d = new MyDelegate(DelegateTest); // d변수는, 함수를 포인팅하는(가리키는) 변수가 됨. // 즉 하나의 메소드가, 매개변수로서 들어올 수 있도록 돕는다. d(4); // d가 DelegateTest를 가르킴. 즉, 이 변수 이름을 부른다 = 함수 호출 // 3. // MyDelegate d1 = new MyDelegate(DelegateTest1); // 위는 불가능. 왜냐하면 MyDelegate라는 타입은, 반환형식은 void, 파라미터는 int 하나를 가르키는 함수에 대해서만 가르킬 수 있다는 것. 이를 해결하기 위해서는 또 다른 delegate를 선언해야 함. MyDelegate1 d1 = new MyDelegate1(DelegateTest1); } private void DelegateTest(int myVal) { Console.WriteLine($"DelegateTest() called {myVal}"); } private int DelegateTest1(int myVal) { return myVal + 1; } } }
[ Windows Forms App 에서 Delegate 예시 ]

[ Form1.cs ]
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } // 이 버튼이 눌러지면, 이벤트가 발생하는데, 그 이벤트(delegate)에다가 button1_Click메소드를 만들고 이 메소드 명을 넣어준 것. // 컴퓨터 뒤에서는 delegate 행위가 수행되고 있는 것 (개발자 입장에서는 보이지 않음) // 즉 button 클래스에는 delegate가 있음. 이것이 button1_Click과 연결되게 된 것 // 다시말해 button(return type : void, 매개변수 sender, e)의 delegate에 button1_Click메소드를 할당해준 것. private void button1_Click(object sender, EventArgs e) // [ object sender, EventArgs e를 사용한다는 것 중요 ] { textBox1.Text = "버튼이 눌러졌어요"; } // delegate로 연결되어 callback이 이루어지도록 만드는 개념 = Event Driven 방식 } }

2) Delegate를 이용한 ConsoleMenu 만들기

- CallBack

ex )
[ Program.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Program { static void Main(string[] args) { Dele d = new Dele(); d.Run(); } } }
[ Dele.cs ]
using Student; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Dele { ConsoleMenu Menu; public Dele() { Menu = new ConsoleMenu(); } public void Run() { // 1. Menu1 : "1" or "Menu1" 입력 -> Delegate 호출 // cf) menu는 1번을 눌렀을 때 무엇을 해야할지 아무것도 모른다. 그러나 그것에 등록된 Delegate를 호출해준다. 즉 Menu에 보편성이 생김. // 2. Menu2 // ... // q : 종료 // 입력 : n CreateMenu(); // 무한루프 돌면서 메뉴를 보여주는 프로그램 - q를 누르면 죽어버림. for (; ; ) { Menu.Show(); Menu.Read(); } } private void CreateMenu() { /* MenuItem item = new MenuItem("1","Menu_Title1",Menu_1_Callback); Menu.MenuList.Add(item); item = new MenuItem("2","Menu_Title2",Menu_2_Callback);*/ // 줄여쓰기 Menu.MenuList.Add(new MenuItem("1", "Menu_Title1", Menu_1_Callback)); // delegate로, Menu_1_Callback이 들어가는 것 Menu.MenuList.Add(new MenuItem("2", "Menu_Title2", Menu_2_Callback)); Menu.MenuList.Add(new MenuItem("q", "프로그램 종료", Quit_Callback)); } // [ 메뉴 1 구현 ] // delegate에서 구현한 시그니처를 그대로 받음. // = public delegate void MenuKeyPressDelegate(object sender, MenuArgs args); // ConsoleMenu에서는 MenuArgs가 아닌 MenuKeyPressArgs(retval)로 보냄. // MenuArgs는 MenuKeyPressArgs의 조상이기 때문에, 이를 가리킬 수 있다. private void Menu_1_Callback(object sender, MenuArgs args) { Console.WriteLine($"Menu_1_Callback 호출됨. sender={sender.ToString()} args={((MenuKeyPressArgs)args).MenuChar}"); // MenuKeyPressArgs로 실제로는 보낸것을 MenuArgs로 받았기 때문에, 다시 자식으로 캐스팅해줌으로써 자식까지 보이도록 해줌. } // [ 메뉴 2 구현 ] private void Menu_2_Callback(object sender, MenuArgs args) { Console.WriteLine($"Menu_2_Callback 호출됨. sender={sender.ToString()} args={((MenuKeyPressArgs)args).MenuChar}"); } // [ 프로그램 종료 구현 ] private void Quit_Callback(object sender, MenuArgs args) { Environment.Exit(0); // 프로그램 빠져나가기 Console.WriteLine($"Quit_Callback 호출됨. sender={sender.ToString()} args={((MenuKeyPressArgs)args).MenuChar}"); Console.WriteLine("Bye"); } } }
[ MenuItem.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class MenuItem { public delegate void MenuKeyPressDelegate(object sender, MenuArgs args); // MenuKeyPressDelegate가 불러지면, callback함수로 날라감. 즉 호출한 클래스에 있는 함수를 부르게 됨. 이 때, 해당 함수에게 seneder(누가 불렀는지)와, MenuArgs 클래스의 args매개변수 전달. // sender : Windows에서는 이벤트 핸들러가 불러줄 적에 사용하는 파라미터가 정형화되어 있음. 이러한 원리로 날라간다는 것을 설명하기 위해 이렇게 작성한 것. public string MenuChar { get; set; } // 1, 2, 3, ... , q public string MenuTitle { get; set; } // Menu1, Menu2, Menu3, ... public MenuKeyPressDelegate KeyPressDelegate { get; set; } // 특정 메뉴를 눌렀을 때, 불러올 함수 => func(sender, args) // 생성자 단축키 : ctor + tab + tab public MenuItem(string menu_char, string menu_title, MenuKeyPressDelegate dele) { MenuChar = menu_char; MenuTitle = menu_title; KeyPressDelegate = dele; } public MenuItem():this(null, null, null){ } } }
[ ConsoleMenu.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class ConsoleMenu { public List<MenuItem> MenuList { get; set; } // (List 컬랙션을 활용) MenuItem을 계속 가질 수 있는 Array형태의 컬랙션 // MenuList각각에는 MenuItem이 들어감 public ConsoleMenu() { MenuList = new List<MenuItem>(); // 컬랙션 생성 } // 호출한 부분의, MenuItem의 menu들을 보여줌 public void Show() { foreach(MenuItem item in MenuList) { Console.WriteLine($"{item.MenuChar}.{item.MenuTitle}"); } Console.WriteLine(); } // menu 선택 public void Read() { Console.Write("메뉴 선택 : "); string retVal = Console.ReadLine(); // 1, 2, ... ,q // 이 값들이 MenuList중 하나에 속해있을 것. foreach (MenuItem item in MenuList) { // 1. MenuItem에서 MenuChar의 값 == 입력한 값 // 2. MenuItem에서 KeyPressDelegate값이 null이 아닐 때 (Delegate 값을 가지고 있는가) if (item.MenuChar == retVal && item.KeyPressDelegate != null) { item.KeyPressDelegate(this, new MenuKeyPressArgs(retVal)); // delegate 호출 } } } } }
[ MenuArgs.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class MenuArgs { } // Menu를 선택할 적에, 파리미터로 전달될 형식을 모델링하는 것. class MenuKeyPressArgs : MenuArgs { public string MenuChar { get; set; } // string으로 입력한 값을 돌려주겠다. public MenuKeyPressArgs(string menu_char) { MenuChar = menu_char; } } }


3) EventHandler를 사용한 ConsoleMenu 프로그램 만들기
- Windows Forms 프로그램에서는 전부 다 EventHandler를 쓰게 됨.
- delegate(low level)보다 EventHandler(high level)를 사용하는 것이 더 효율적임.
[ Program.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Program { static void Main(string[] args) { Dele d = new Dele(); d.Run(); } } }
[ Dele.cs ]
using Student; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class Dele { ConsoleMenu Menu; public Dele() { Menu = new ConsoleMenu(); } public void Run() { CreateMenu(); for (; ; ) { Menu.Show(); Menu.Read(); } } private void CreateMenu() { MenuItem item = new MenuItem("1", "Menu_Title1"); item.MenuKeyPressEventHandler += Menu_1_Callback; Menu.MenuList.Add(item); item = new MenuItem("2", "Menu_Title2"); item.MenuKeyPressEventHandler += Menu_2_Callback; Menu.MenuList.Add(item); item = new MenuItem("q", "프로그램 종료"); item.MenuKeyPressEventHandler += Quit_Callback; Menu.MenuList.Add(item); } private void Menu_1_Callback(object sender, EventArgs args) { Console.WriteLine($"Menu_1_Callback 호출됨. sender={sender.ToString()} args={((MenuKeyPressArgs)args).MenuChar}"); } private void Menu_2_Callback(object sender, EventArgs args) { Console.WriteLine($"Menu_2_Callback 호출됨. sender={sender.ToString()} args={((MenuKeyPressArgs)args).MenuChar}"); } private void Quit_Callback(object sender, EventArgs args) { Environment.Exit(0); // 프로그램 빠져나가기 Console.WriteLine($"Quit_Callback 호출됨. sender={sender.ToString()} args={((MenuKeyPressArgs)args).MenuChar}"); Console.WriteLine("Bye"); } } }
[ MenuItem.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class MenuItem { // [ Delegate 사용 X ] // public delegate void MenuKeyPressDelegate(object sender, EventArgs args); // [ EventHandler 등록 ] public event EventHandler MenuKeyPressEventHandler; // EventHandler 변수인, MenuKeyPressEventHandler // EventHandler는 이미 delegate를 모두 가지고 있음. public string MenuChar { get; set; } // 1, 2, 3, ... , q public string MenuTitle { get; set; } // Menu1, Menu2, Menu3, ... // [ Delegate 사용 X ] //public MenuKeyPressDelegate KeyPressDelegate { get; set; } public MenuItem(string menu_char, string menu_title) { MenuChar = menu_char; MenuTitle = menu_title; } public MenuItem():this(null, null){ } public void CallEvent(object sender, string args) { // EventHandler는 자신이 선언된 쪽에서 처리가 되어야 함. 다른 클래스에서 처리하는 것은 허락하지 않음. // 따라서, ConsoleMenu.cs에서 사용할 수 없고, 여기서 사용해야 함. if (MenuKeyPressEventHandler != null) { MenuKeyPressEventHandler(sender, new MenuKeyPressArgs(args)); } } } }
[ ConsoleMenu.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { class ConsoleMenu { public List<MenuItem> MenuList { get; set; } public ConsoleMenu() { MenuList = new List<MenuItem>(); } // 호출한 부분의, MenuItem의 menu들을 보여줌 public void Show() { foreach(MenuItem item in MenuList) { Console.WriteLine($"{item.MenuChar}.{item.MenuTitle}"); } Console.WriteLine(); } // menu 선택 public void Read() { Console.Write("메뉴 선택 : "); string retVal = Console.ReadLine(); // 1, 2, ... ,q foreach (MenuItem item in MenuList) { if (item.MenuChar == retVal) { item.CallEvent(this, retVal); } } } } }
[ MenuArgs.cs ]
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace class_0804 { /* class MenuArgs { }*/ class MenuKeyPressArgs : EventArgs //.Net Framework에서는 EventArgs라는 것을 지원해줌. MenuArgs를 따로 지정할 필요 X // EventArgs를 상속하여, 여러 다양한 종류의 파라미터를 받을 수 있음. { public string MenuChar { get; set; } // string으로 입력한 값을 돌려주겠다. public MenuKeyPressArgs(string menu_char) { MenuChar = menu_char; } } }
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |