[문과 코린이의 IT 기록장] C# - 클래스 2 (구조체 , 제네릭 (Generic), Delegate / Event)
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;
}
}
}
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |