Search
Duplicate

C# 입문 2. 메소드(함수), 클래스와 객체

간단소개
C#의 메소드와 클래스
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
C#
태그
C#
Scrap
8 more properties

알립니다

1. 이 글은 <이것이 C#이다>를 바탕으로 쓰여졌으며,
2. ft_printf 과제를 통과하셨다면 큰 무리 없이 읽을 수 있게 작성됐습니다.
3. 자세한 설명이 필요하시다면 검색과 MS의 C# 자습서를 참고하시거나, 혹은 클러스터의 집현전과 42서울 전자도서관에서 E-Book 대여로 <이것이 C#이다>를 빌려보시는 것을 추천합니다.
4. 오류가 있거나, 질의가 있으시다면 Slack(sangtale)으로 DM 주시길 바랍니다!

컴파일(빌드)부터 개발환경 세팅은 아래의 글을 참고하시길 바랍니다.

Mac

조금 특별한 개념이라면 직접 타이핑하고 컴파일 해보시는 것을 추천드립니다
저번 글의 만들어 보기 코드!

메소드 Method

메소드란?

메소드는 객체지향 프로그래밍에서 사용하는 용어로, C와 C++에서는 함수(Function)라고 불렸습니다. 각설하고 아래와 같이 선언합니다.
class 클래스_이름 { //한정자(Modifier): 메소드의 접근(액세스)을 수식, 한정한다. 한정자 반환_형식 메소드_이름(매개변수 목록) { // 실행하고자 하는 코드 return (메소드_결과); } }
C#
간단히 코드를 작성해 볼까요?
using static System.Console; namespace Hello { class MainApp { public static void Mean(double a, double b, double c, double d, ref double mean) { mean = a + b + c + d; } static void Main(string[] args) { double mean = 0; Mean(1, 2, 3, 4, ref mean); Console.WriteLine(mean); } } }
C#
이 코드에서 Mean 메소드는 리턴 값이 없습니다.
낯선 친구가 등장하는데요, 바로 ref!
ref의 쓰임새는 C언어의 포인터(*)와 일치합니다.
어떤 값을 참조(reference)하겠다는 의미입니다.
코드를 실행하면 메소드 Mean 에 들어간 mean의 0에서 값이 10으로 변환되어 나옵니다.

메소드 가변인자

ft_printf 과제를 하셨다면, 가변인자에 대해서 잘 아시리라 생각합니다.
그리고 저번 마지막 문제에서 Main 메소드에 주어진 가변인자(string args)를 통해 코드를 짜셨으리라, 맞죠…?
아무튼 C#의 메소드 역시 가변인자를 처리할 수 있습니다.

가변인자를 모르신다면?

using static System.Console; namespace Hello { class Product { public int Sum(params int[] args) { int sum = 0; for (int i = 0; i < args.Length; i++) sum += args[i]; return (sum); } } class MainApp { static void Main(string[] args) { Product product = new Product(); WriteLine(product.Sum(123, 123, 123)); } } }
C#
위처럼 params 키워드로 활용할 수 있습니다.
혹시 한 가지 의문점에 직면하지 않으셨나요? ‘가변인자의 자료형이 int가 아니라면?’

메소드 오버로딩

위의 필요성에 의해 나온 것이 메소드 오버로딩입니다!
오버로딩(Overloading)에는 과적이라는 의미가 있습니다만, 프로그래밍에서는 다중 정의라고 해석합니다. ‘메소드 과적’이라는 말보다는 ‘메소드 다중 정의’라는 말이 적확하죠?
using static System.Console; namespace Hello { class Product { public int Sum(params int[] args) { int sum = 0; for (int i = 0; i < args.Length; i++) sum += args[i]; return (sum); } public double Sum(params double[] args) { double sum = 0; for (int i = 0; i < args.Length; i++) sum += args[i]; return (sum); } public float Sum(params float[] args) { float sum = 0; for (int i = 0; i < args.Length; i++) sum += args[i]; return (sum); } } class MainApp { static void Main(string[] args) { Product product = new Product(); WriteLine(product.Sum(123, 123, 123)); WriteLine(product.Sum(1.23, 1.23, 1.23)); WriteLine(product.Sum(1.11111111111, 1, 1.222222222222)); } } }
C#
위처럼 메소드를 다중 정의하고 호출을 하면 컴파일 단계에서 컴파일러가 인자의 자료형에 맞춰 올바른 메소드를 호출해 줍니다. 쉽죠?
이 밖에도 C#에는 C와 다른, 새로운 개념을 제시합니다!
몇 개를 살펴보고 가시죠.

출력 전용 매개변수

void Divide(int a, int b, out int quotient, out int remainder) { quotient = a / b; remainder = a % b; }
C#
out 키워드를 통하여, ref와는 다르게 메소드의 반환값을 여러 개 받을 수 있습니다.
void Divide(int a, int b, ref int quotient, ref int remainder) { quotient = a / b; remainder = a % b; }
C#
그렇다면, 이 코드와 차이점은 뭘까요?
1.
out 키워드를 적으면 ref와는 다르게 인자를 주지 않을 시에 컴파일러가 경고를 한다.
2.
메소드를 호출할 때 초기화 하지 않은 지역 변수를 메소드의 out 매개변수로 넘길 수 있다.
이런 차이점이 있습니다!
잘 모르시겠다면, 직접 두 메소드를 만들고 인자를 a, b만 줘보셔요!

명명된 인수

using static System.Console; namespace NamedParam { class MainApp { static void PrintCadet(string Name, int Level) { WriteLine($"Cadet : {Name}, Lv : {Level}"); } static void Main(string[] args) { PrintCadet(Name : "Sangtale", Level : 0); } } }
C#
메소드에 정의된 인수 이름에 맞춰서 콜론(:)으로 호출과 동시에 초기화 & 대입할 수 있습니다.
순서와 자료형에 유의하세요!

선택적 인수

C# 메소드의 매개변수는 기본값을 가질 수 있습니다. 이를 선택적 인수라고 합니다.
using static System.Console; namespace OptionalArg { class MainApp { static void MyMethod_0(int a, int b = 42) { WriteLine($"{a}, {b}"); } static void MyMethod_1(int a = 42) { WriteLine($"{a}"); } static void Main(string[] args) { MyMethod_0(24); MyMethod_0(24, 24); MyMethod_1(); MyMethod_1(0); } } }
C#
출력 결과
>> 24, 42 >> 24, 24 >> 42 >> 0
C#
P.S. 선택적 인수는 필수 인수의 뒤에 와야 합니다.
// 선택적 인수는 필수 인수의 뒤에 와야 합니다. static void MyMethod_0(int a = 42, int b) { WriteLine($"{a}, {b}"); }
C#

클래스

객체지향 프로그래밍을 모르신다면?

클래스란?

클래스는 객체(object, instance)를 만들기 위한 청사진입니다. 이미지에서 객체는 자전거 바퀴, 자전거 몸통, 그리고 핸들로 객체를 두고 있네요. 설계도(클래스)에 따라서 부품(객체)의 재질, 색깔, 크디 등을 유동적으로 둘 수도 있고 안 둘 수도 있겠죠.
조금 생소한 개념을 설명해 드리겠습니다.
string digit = "0123456789"; string greeting = "Hello";
C#
이 코드에서 string문자열을 다루는 클래스이고 digitgreeting은 객체가 됩니다.
다시 말하자면, string은 문자열 객체를 다루기 위한 청사진이고, digitgreeting은 실제로 데이터를 담을 수 있는 객체입니다. 또한 위의 코드에서 digitgreeting은 실제로 데이터를 담은 실제 객체입니다.
이렇게 데이터를 가지고 있는 객체를 일컬어서 string(혹은 다른 클래스)의 실체(instance)라고 합니다. 일반적으로 영어 발음대로 ‘인스턴스’라고 부릅니다. 그래서 객체를 인스턴스라고 부르기도 합니다. 또한 클래스를 통해서 객체를 만드는 것을 ‘인스턴스화’(instantiate)라고 부릅니다.
자, 따분한 설명은 이쯤으로 하고 클래스부터 선언해 볼까요?

클래스 선언하고 가지고 놀기

클래스 선언

세부적인 요소는 생략하고 간략하게 피시너 클래스를 만들어 볼까요?
1. 피신을 시작할 때 평가 포인트는 5점, 레벨은 0이어야겠죠?
2. 이 클래스는 필수적으로 인트라 ID가 있어야 할 것입니다. 이게 인스턴스들의 차이점이 될 것입니다.
3. 또한 동료평가(Evaluating)이라는 메소드를 수행해야 합니다. 겸사겸사 평가 포인트가 벌려야겠죠?
class Pisciner { public string Name; public int Level = 0; public int EvalPoints = 5; public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } }
C#
이렇게 코드가 나왔네요! 위에서 1, 2, 3의 프로세스를 거쳐 클래스의 필요조건, 충분조건을 코드로 적거나 혹은 그 과정을 추상화(抽象化, abstraction)라고 합니다.
추상화 : 중요한 특징을 찾아낸 후 간단하게 표현하는 것.
자, 금방 선언한 이 클래스를 통해 저희는 피시너를 양산할 수 있습니다!
코드를 마저 완성해 볼까요?
using static System.Console; namespace ClassInstance { class MainApp { class Pisciner { public string Name; public int Level = 0; public int EvalPoints = 5; public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } } static void Main(string[] args) { Pisciner sangtale = new Pisciner(); sangtale.Name = "sangtale"; sangtale.Evaluating(100, "대박... 설명 정말 잘 하셨습니다!"); Console.WriteLine(sangtale.EvalPoints); Pisciner taeng = new Pisciner(); taeng.Name = "taeng"; taeng.Evaluating(-42, "치팅인 것 같습니다. TIG를 받으십시오."); Console.WriteLine(taeng.EvalPoints); } } }
C#
자, 코드를 위처럼 완성했습니다. 클래스를 생성하시는 것은 이미 익숙하시리라 생각합니다.
이제 미루던 설명을 이어나가겠습니다.

생성자

Pisciner sangtale = new Pisciner();
C#
클래스를 통해 객체를 생성할 때는 위와 같이 적습니다.
여기서 Pisciner()는 생성자(constructor)라고 합니다. (cpp평가를 다녀보셨다면 익숙하실 겁니다)
자, 컴파일을 해볼까요?
정상적으로 평가 포인트가 증가했네요.
생성자에 대해 설명을 조금 이어가자면, C#의 생성자는 명시적으로 작성하지 않아도 컴파일러가 자동으로 생성해 줍니다.
Pisciner sangtale = new Pisciner(); sangtale.Name = "sangtale";
C#
그런데 위 코드에서 생성자를 호출하고 일일이 피시너의 이름을 초기화 해야 하는 단점이 생기죠?
그런 단점을 보완하기 위해 프로그래머가 생성자를 컴파일러에게 맡기지 않고 만들 수도 있습니다.
class Pisciner { public string Name; public int Level = 0; public int EvalPoints = 5; public Pisciner() { Name = ""; } public Pisciner(string _Name) { Name = _Name; } public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } }
C#
생성자 역시 메소드 오버로딩을 지원합니다. 인자가 없을 시에 기본 생성자를 만들고, Name이라는 인자가 주어졌을 시에 초기화해 주는 생성자를 만들면 끝입니다. 쉽죠?
using static System.Console; namespace ClassInstance { class MainApp { class Pisciner { public string Name; public int Level = 0; public int EvalPoints = 5; public Pisciner() { Name = ""; } public Pisciner(string _Name) { Name = _Name; } public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } } static void Main(string[] args) { Pisciner sangtale = new Pisciner("sangtale"); sangtale.Evaluating(100, "대박... 설명 정말 잘 하셨습니다!"); Console.WriteLine(sangtale.EvalPoints); Pisciner taeng = new Pisciner("taeng"); taeng.Evaluating(-42, "치팅인 것 같습니다. TIG를 받으십시오."); Console.WriteLine(taeng.EvalPoints); } } }
C#
코드가 짧아졌네요! 컴파일 해볼까요?
정상적으로 초기화, 출력이 되는 모습입니다!
매개변수를 받는 생성자를 정의할 경우에 컴파일러가 매개변수를 받지 않는 기본 생성자를 제공하지 않습니다! 컴파일러가 생성자가 정의됐을 경우에는 ‘클래스가 특별하게 초기화 되기를 원했다’고 인식해서 이전과 다르게 기본 생성자를 제공하지 않습니다. 따라서 매개변수를 받지 않는 기본 생성자 역시 정의해 줘야 합니다.

종료자(소멸자)

클래스의 생성이 있다면, 죽음도 있어야겠죠? 바로 종료자(destructor)입니다(C++에서는 소멸자라고 불렸죠).
클래스 이름 앞에 ~를 붙여서 사용합니다.
~Pisciner() { //실행할 코드 }
C#
종료자는 한정자와 매개변수도 없고, 메소드 오버로딩도 불가능합니다.
심지어는 직접 호출할 수도 없습니다!

C#에서 과연 종료자를 구현해야 할까요?

C#에는 가비지 컬렉터가 있습니다. 가비지 컬렉터가 동작할 시점을 알 수가 없다는 점 때문에 가급적 종료자를 구현하지 않는 것이 좋습니다. 객체의 사용이 끝난 직후가 될 수도 있고 10분 후가 될 수도 있습니다. 또한 명시적으로 종료자가 구현되어 있다면 가비지 컬렉터가 object로부터 상속받은 Finalize() 메서드를 클래스의 족보를 타고 올라가며 호출하기 때문에 성능의 저하만 가져올 확률이 높습니다.
using static System.Console; namespace ClassInstance { class MainApp { class Pisciner { public string Name; public int Level = 0; public int EvalPoints = 5; public Pisciner() { Name = ""; } public Pisciner(string _Name) { Name = _Name; } /// 종료자 시작 ~Pisciner() { Console.WriteLine("Good Bye, World!"); } /// 끝 public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } } static void Main(string[] args) { Pisciner sangtale = new Pisciner("sangtale"); sangtale.Evaluating(100, "대박... 설명 정말 잘 하셨습니다!"); Console.WriteLine(sangtale.EvalPoints); Pisciner taeng = new Pisciner("taeng"); taeng.Evaluating(-42, "치팅인 것 같습니다. TIG를 받으십시오."); Console.WriteLine(taeng.EvalPoints); } } }
C#
만약 종료자가 호출이 된다면, 콘솔창에 “Good Bye, World!”를 출력할 것입니다. 그렇지만 보통 이렇게 짧은 코드로는 가비지 콜렉터가 종료자가 호출이 될 일은 없을 것입니다.

정적(Static) 필드와 메소드

그동안 static이라는 한정자를 자주 써왔습니다. 과연 필드는 뭐고, static이 뭘까요?

필드(field)

class Pisciner { public string Name; public int Level = 0; public int EvalPoints = 5; public Pisciner() { Name = ""; } public Pisciner(string _Name) { Name = _Name; } public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } }
C#
Name, Level, 그리고 EvalPoint처럼 클래스 안에서 선언된 변수들을 일컬어 필드(field)라고 합니다. 그리고 필드와 메소드를 비롯하여 프로퍼티, 이벤트(나중에 소개하겠습니다) 등 클래스 내에 선언된 요소들을 멤버라고 합니다. C/C++의 구조체와 비슷하죠?

static

static은 사전적으로 정적(靜的)이라는 뜻을 가지고 있습니다. ‘움직이지 않는다’는 뜻이죠.
static 한정자를 사용하게 되면, 메소드나 필드가 ‘클래스의 인스턴스’가 아닌 클래스에 소속되도록 할 수 있습니다.
음, 잘 이해가 안 가시나요? 만약 위 코드에서 ‘이벤트 풀’을 만든다면 어떨까요? 피시너들이 포인트를 기부하고, 기부한 포인트가 쌓이는 그 풀 말이죠!
class Pisciner { static public int EventPools = 0; public string Name; public int Level = 0; public int EvalPoints = 5; public Pisciner() { Name = ""; } public Pisciner(string _Name) { Name = _Name; } public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } }
C#
자, 이벤트 풀이 만들어졌습니다. static 한정자를 통해 피시너들끼리 공유할 수 있는 필드, 그러니 ‘이벤트 풀’이라는 변수를 공유할 수 있게 된 것이죠.
이제 자신의 평가 포인트를 기부하면서 0으로 초기화된 저 EventPools를 채울 수 있도록 메소드를 만들어 볼까요?
class Pisciner { static public int EventPools = 0; public string Name; public int Level = 0; public int EvalPoints = 5; public Pisciner() { Name = ""; } public Pisciner(string _Name) { Name = _Name; } public void Evaluating(int point, string Feedback) { Console.WriteLine($"점수 : {point}"); Console.WriteLine($"{Name}'s Feedback : {Feedback}"); EvalPoints++; } public void Providing(int Num) { if (Num > EvalPoints) { Console.WriteLine($"{Num}만큼 기부할 평가 포인트가 없어요!"); return ; } Console.WriteLine($"{Name} provided {Num} points to the pool."); EventPools += Num; EvalPoints -= Num; } }
C#
자, 그러면 taeng님의 포인트를 몰수해 볼까요?
아래의 코드로 메인 메소드를 고치고 호출해 보겠습니다.
static void Main(string[] args) { Pisciner taeng = new Pisciner("taeng"); taeng.Evaluating(-42, "치팅인 것 같습니다. TIG를 받으십시오."); Console.WriteLine($"탱님의 평가 포인트 : {taeng.EvalPoints}"); taeng.Providing(6); Console.WriteLine($"이벤트 풀의 평가 포인트 : {Pisciner.EventPools}"); Console.WriteLine($"탱님의 평가 포인트 : {taeng.EvalPoints}"); }
C#
자, 이제 새로운 피시너를 만들고 평가 포인트 압수… 아니… 기부를 시켜보시죠!
다음 시간에는 클래스에 대해서 조금 더 상세하게 다루겠습니다!

만들어 보기

1. 이제 이펜트 풀이 구현 됐으니, 이벤트 풀이 열리는 메소드와 프로세스를 구현해 보시면 됩니다!
2. 또한 이벤트 풀 전용 Evaluating 메소드도 필요하겠죠? (평가 시 포인트 x2, 이벤트 풀에서 -1)
3. 더 연습해 보고 싶으시다면, Subject(sh00, sh01, c00 등)의 클래스를 구현해 보시는 것은 어떠신가요? Subject 클래스를 Pisciner의 인스턴스들이 사용할 수 있어야겠죠?