객체 지향 문법
클래스
개발자가 직접 속성과 메서드로 구성된 타입을 정의한 것이다. 클래스로 정의된 타입은 모두 참조형으로 분류되므로 값이 힙에 저장된다.
필드
클래스의 속성을 변수로 정의한 것이다. 기본형 타입이나 사용자 정의형 타입으로 정의할 수 있다.
메서드
클래스의 행위를 함수로 정의한 것이다.
생성자
객체가 생성되는 시점에 자동으로 호출되는 메서드. 반환타입을 명시하지 않는 점을 제외하면 일반 메서드를 정의하는 방법을 따른다.
소멸자
객체가 제거되는 시점에 자동으로 호출되는 메서드. C#은 C++와 달리 delete라는 예약어가 없고, GC가 호출돼야 소멸자가 호출된다. GC가 정확히 언제 호출되는지 알 수 없기 때문에 소멸자의 사용은 권장되지 않는다. 대신 IDisposable을 구현하여 Dispose()를 사용하는 방법이 권장된다.
다음은 생성자와 소멸자의 예시이다.
class Book
{
public Book() //생성자
{
//자원 생성(비관리 영역의 메모리에 할당된 자원을 말한다.)
}
~Book() //소멸자
{
// 자원 해제(관리 영역의 메모리는 GC가 정리하지만 비관리 영역이 메모리는 정리 해줘야한다. 하지만 소멸자 보다는 IDisposable구현 권장.)
}
}
정적 멤버, 인스턴스 멤버
class Dog
{
static public int CountOfInstance; //Dog 클래스의 총 인스턴수 개수를 세기 위한 정적 필드
public string _name;
public Dog(string name)
{
CountOfInstance++; //객체가 생성될 때 CountOfInstance에 1씩 더해준다.
_name = name;
}
public static void PrintBark()
{
Console.WriteLine(CountOfInstance + " 마리 개가 짖습니다.");
//정적 메서드 또한 호출 시 Dog.PrintBark() 으로 호출한다. 정적 메서드 안에서는 인스턴스 멤버에 접근 불가
}
public static void Main()
{
Console.WriteLine(Dog.CountOfInstance);
Dog blackDog = new Dog("검둥개");
Dog whiteDog = new Dog("백구");
Console.WriteLine(Dog.CountOfInstance);
//CountOfInstance가 인스턴트 필드였다면 인스턴스 단위로 변수를 가지고 있기 때문에 총 인스턴스 개수를 셀 수 없다.
}
}
특정 클래스의 인스턴스를 단 한 개만 만들고 싶은 경우에는
class Dog
{
public static Dog KingDog = new Dog("왕개");
public string _name;
private Dog(string name) //생성자를 private으로 해놓으면 다른 곳에서 객체 생성 불가
{
_name = name;
}
public void Bark() //이 메서드를 쓰려면 Dog.KingDog.Bark() 이런식으로 호출 가능하다.
{
//멍멍
}
}
Main 메서드의 인자로 사용되는 string 배열 사용 예제
public void Bark()
{
Console.WriteLine("첫번째 인자로 멍멍이 입력되었습니다. 멍멍!");
}
public static void Main(string[] args)
{
if (args[0].Equals("멍멍"))
{
Dog.KingDog.Bark();
}
}
다음과 같은 실행 결과를 확인할 수 있다.
정적 생성자
정적 생성자는 정적 데이터를 초기화하거나 한 번만 수행하면 되는 특정 작업을 수행하는 데 사용한다. 특징은 다음과 같다.
- 접근 제한자와 매개변수를 갖지 않는다.
- 첫 번째 인스턴스가 만들어지기 전이나 정적 멤버가 참조되기 전에 클래스를 초기화하기 위해 자동으로 호출된다.
- 직접 호출할 수 없다.
- 사용자는 프로그램에서 정적 생성자가 실행되는 시기를 제어할 수 없다.
class StaticConcTest
{
public static void Main()
{
Dog dog1 = new Dog(); //static, 일반 생성자 모두 호출
Dog dog2 = new Dog(); //일반 생성자만 호출
}
}
public class Dog
{
static bool firstDogCreated = false;
static Dog()
{
Console.WriteLine("static 생성자");
firstDogCreated = true;
Console.WriteLine(firstDogCreated);
Console.WriteLine("첫 번째 개가 태어났습니다.");
}
public Dog()
{
Console.WriteLine("일반 생성자");
Console.WriteLine("개가 태어났습니다.");
}
}
네임스페이스 , using
자바의 패키지 개념과 유사하다. 패키지와 다른 점은 네임스페이스와 폴더가 꼭 대응되지 않아도 되는 것이다. 이름이 같지만 소속도 다르고 실제 쓰임도 다른 클래스들의 이름 충돌을 막기 위해서 사용한다. 그리고 클래스들의 소속을 구분하는데 사용되는 것이 더 일반적이다.
namespace NamespaceEx1
{
class Dog
{
public Dog()
{
Console.WriteLine("네임스페이스1");
}
}
}
namespace NamespaceEx2
{
class Dog
{
public Dog()
{
Console.WriteLine("네임스페이스2");
}
}
}
//dog3를 위해선 여기서 using으로 어떤 네임스페이스를 쓸 건지 선언해줘야 함.
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
NamespaceEx1.Dog dog1 = new NamespaceEx1.Dog(); //"네임스페이스1" 출력
NamespaceEx2.Dog dog2 = new NamespaceEx2.Dog(); //"네임스페이스2" 출력
Dog dog3 = new Dog(); //에러 코드 상단에 using으로 어떤 네임스페이스를 선언하냐에 따라서 다른 결과 출력
}
}
}
캡슐화
접근 제한 유형
이름 | 의미 |
---|---|
internal | 동일한 어셈블리 내에서는 public에 준한 접근을 허용한다. 다른 어셈블리에서는 접근할 수 없다. |
internal protected | protected와 internal의 조합이다. 동일 어셈블리 내에서 정의된 파생 클래스에만 접근을 허용한다. |
접근 제한자를 명시하지 않은 경우에는 클래스는 internal, 클래스 내부의 멤버들은 private으로 설정된다.
class Dog //접근 제한자 생략한 상태는 internal로 되기 때문에 객체 생성이 가능하나, private으로 접근 제한자를 바꾼다면 에러 발생.
{
void bark()
{
Console.WriteLine("멍멍!");
}
}
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
dog.bark(); //bark() 메서드의 접근 제한자가 생략되서 private으로 설정되었기 때문에 호출 불가.
}
}
프로퍼티
접근자/설정자를 편하게 정의하기 위해서 C#에서 제공하는 문법. (attribute(field)와 똑같이 속성으로 번역되기 때문에 주의)
class Dog
{
private string name;
public string Name //Visual Studio에서 자동생성 가능.
{
get
{
Console.WriteLine("get호출");
return name;
}
set
{
Console.WriteLine("set호출");
name = value;
}
}
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
dog.Name = "검둥개"; //"set호출" 출력
string name = dog.Name; //"get호출" 출력
}
}
상속
class Animal
{
private string name;
private void Roam() //private으로 선언되어 있어서 다른 클래스에서 호출 불가.
{
Console.WriteLine("돌아다니기");
}
public void Eat() //public으로 선언되어 있어서 다른 클래스에서 호출 가능
{
Console.WriteLine("쩝쩝");
}
}
class Dog : Animal
{
protected string specific= "개"; //protected로 선언되어 있어서 Dog를 상속받는 BlackDog 클래스에서만 접근 가능.
public void Bark()
{
Console.WriteLine("멍멍!");
}
}
class BlackDog : Dog
{
public void PrintSpecific()
{
Console.WriteLine("나는 " + specific + "과에 속합니다."); //Dog클래스의 specific에 접근하여 읽어옴.
}
}
sealed class Cat : Animal
{
public void Meow()
{
Console.WriteLine("야옹!");
}
}
//class BlackCat : Cat //Cat클래스가 sealed 되어있기 때문에 상속 불가
//{
//}
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
dog.Eat(); //Animal 클래스에서 상속받은 메서드. "쩝쩝" 출력.
dog.Bark();
//dog.Roam(); //private로 선언되어 있어서 호출 불가.
BlackDog blackDog = new BlackDog();
blackDog.PrintSpecific(); //protected로 선언된 specific 변수에 접근해서 "개"를 읽어옴. "나는 개과입니다." 출력.
Cat cat = new Cat();
cat.Eat(); //Animal 클래스에서 상속받은 메서드. "쩝쩝" 출력.
cat.Meow();
}
}
형변환
- as 연산자 : 형변환이 가능하면 지정된 타입의 인스탄스 값 반환, 그렇지 않으면 null 반환.
- is 연산자 : 형변환이 가능하면 true, 그렇지 않으면 false 반환.
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
Dog dog = animal as Dog;
Cat cat = new Cat();
if (dog == null) //animal은 Dog으로 변환 불가해서 null을 반환하므로 실행된다.
{
Console.WriteLine("Animal은 Dog로 변환 불가능!");
}
if (cat is Animal)
{
Console.WriteLine("cat는 Animal이다."); //변환 가능하므로 실행됨
}
if (animal is Cat)
{
Console.WriteLine("animal은 Cat이다."); //변환이 안되므로 실행 안됨
}
}
}
System.Object
자바의 Object 클래스처럼 C#에도 모든 타입의 조상격인 object 타입이 있다. object는 참조형인데 값 형식의 부모 타입이기도 하다. 이런 불일치를 해소하기 위해서 모든 값 형식은 object 밑에 존재하는 System.ValueType에서 상속받게 하고있다. 이를 도식화하면 다음과 같다.
따라서 참조형식 = (object로부터 상속받는 모든 타입) - (System.ValueType로부터 상속받는 모든 값 타입) 이다.
C#에서 정의되는 모든 형식은 object로 변환하고 다시 되돌리는 것이 가능하다.
ToString
static void Main(string[] args)
{
Animal animal = new Animal();
Console.WriteLine(animal.ToString()); //클래스의 전체 이름인 ConsoleApplication1.Animal 출력.
Console.WriteLine(animal.numberOfAnimal.ToString()); //기본 타입에선 갖고 있는 값 출력.
}
GetType
static void Main(string[] args)
{
Animal animal = new Animal();
int num = 5;
Type type = animal.GetType(); //Animal 클래스의 정보를 가지고 있는 System.Type의 인스턴스 호출.
Type intType = num.GetType(); //기본 자료형도 호출 가능.
Console.WriteLine(type.FullName); //ConsoleApplication1.Animal 출력.
Console.WriteLine(type.IsClass); //true 출력.
Console.WriteLine(intType.FullName); //System.Int32 출력.
Console.WriteLine(typeof(Animal).FullName);
//typeof 예약어는 클래스의 이름에서 Type 반환. ConsoleApplication1.Animal 출력.
}
Equals
자바와 동일. 주의할 점은 값 형식은 해당 인스턴스가 소유하고 있는 값을 대상으로 비교한다. 참조 형식은 할당된 메모리 위치를 가리키는 값이 같은지 비교한다.
static void Main(string[] args)
{
int num1 = 5;
int num2 = 5;
int num3 = 6;
Console.WriteLine(num1.Equals(num2)); //가리키는 값이 같으므로 True.
Console.WriteLine(num3.Equals(num2)); //가리키는 값이 다르므로 False.
Animal animal1 = new Animal();
Animal animal2 = new Animal();
Animal animal3 = animal1;
Console.WriteLine(animal1.Equals(animal2)); //서로 가리키는 힙 메모리의 위치가 다르기 때문에 False.
Console.WriteLine(animal1.Equals(animal3)); //서로 같은 위치를 가리키고 있기 떄문에 True.
}
GetHashCode
특정 인스턴스를 고유하게 식별할 수 있는 4바이트 int 값을 반환한다. Equals의 참/거짓 판단은 이 GetHashCode값을 기준으로 이루어진다.
static void Main(string[] args)
{
short num1 = 256;
int num2 = 256;
short num3 = 256;
Console.WriteLine(num1.GetHashCode()); //num1과 num3는 같은 값을 가리키므로 HashCode값도 일치한다.
Console.WriteLine(num3.GetHashCode());
Console.WriteLine(num2.GetHashCode()); //int 형은 HashCode와 범위값이 일치하므로 그대로 반환하게 설정됨.
Animal animal1 = new Animal();
Animal animal2 = new Animal();
Console.WriteLine(animal1.GetHashCode()); //힙 메모리 내의 다른 주소를 참조하므로 서로 다른 값을 반환한다.
Console.WriteLine(animal2.GetHashCode());
}
System.Array
모든 배열은 Array 타입을 조상으로 둔다. 일부 프로퍼티와 메서드의 사용 예시다.
static void Main(string[] args)
{
int[] intArray = new int[] { 2, 3, 1, 6, 4, 5, 2 };
int i = 0;
Console.WriteLine(intArray.Rank); //Array.Rank(배열의 차수를 구한다.) 배열의 차수 1.
Console.WriteLine(intArray.Length); //Array.Length(배열의 길이를 구한다.) 배열의 길이 7.
foreach (int num in intArray)
{
Console.Write(" "+intArray.GetValue(i++)); //Array.GetValue(i)(배열에서 i번째 값을 리턴한다.) //정렬 전 배열 출력
}
Array.Sort(intArray); //Array.Sort(정렬 하려는 배열)(배열을 오름차순 정렬한다.) //배열 정렬
i = 0;
foreach (int num in intArray)
{
Console.Write(" " + intArray.GetValue(i++)); //정렬 후 배열 출력
}
int[] copyArray = new int[intArray.Length];
Array.Copy(intArray, copyArray, 5); //Array.Copy(원본 배열, 복사 배열, 복사하려는 길이) //intArray에서 copyArray로 5개만 복사
i = 0;
foreach (int num in copyArray)
{
Console.Write(" " + copyArray.GetValue(i++)); //복사 후 배열 출력
}
}
}
this
여러개의 생성자에서 지정해둔 하나의 private 메서드로 접근하여 처리하는 방법도 있다.
class Book
{
string title;
decimal isbn;
string author;
public Book(string title) : this(title, 0) { } //this 예약어를 이용해 생성자 내에서 다른 생성자 호출
public Book(string title, decimal isbn) : this(title, isbn, string.Empty) { }
public Book() : this(string.Empty, 0, string.Empty) { }
public Book(string title, decimal isbn, string author)
{
this.title = title;
this.isbn = isbn;
this.author = author;
}
}
정적 멤버에선 this 예약어를 사용할 수 없다. this는 new로 할당된 '객체'를 가리키는 내부 식별자이기 때문이다.
base
자바의 super 예약어와 같다. this와 용법은 같지만 base는 가리키는 대상이 부모 클래스이다.
다형성
오버라이딩
class Animal
{
virtual public void Sound() //virtual : 자식 클래스에 의해서 재정의 될 수 있다.
{
Console.WriteLine("소리를 낸다.");
}
}
class Dog : Animal
{
override public void Sound() // override : 부모로부터 상속받은 메서드와는 다르게 구현한다.
{
Console.WriteLine("멍멍!");
}
}
class Cat : Animal
{
override public void Sound()
{
Console.WriteLine("야옹!");
}
}
class Human : Animal
{
override public void Sound()
{
Console.WriteLine("안녕하세요!");
}
}
class Whale : Animal
{
new public void Sound()
{
Console.WriteLine("끼룩끼룩!"); //단순히 자식 클래스에서 동일한 이름의 메서드가 필요했을 때 new를 쓴다.
}
}
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
Cat cat = new Cat();
Human human = new Human();
Whale whale = new Whale();
Animal animal = human;
dog.Sound(); //자식 클래스의 인스턴스에 따라 다양하게 재정의 된다.
cat.Sound();
animal.Sound(); //오버라이딩을 했기 때문에 자식 클래스의 메서드 호출.
animal = whale;
animal.Sound(); //new 했기 때문에 부모 클래스의 원 메서드 호출.
}
}
base를 이용한 메서드 재사용
자식클래스에서 base를 이용하여 부모 클래스의 메서드를 호출하거나, 호출하지 못하게 강제할 수 있는 방법이 없으므로, 상위 클래스의 도움말을 잘 확인하여 오버라이드 해야한다.
override public void Sound()
{
base.Sound(); //base 예약어를 이용하면 부모 클래스의 메서드를 호출한 후 자식 클래스의 메서드를 호출한다.
Console.WriteLine("야옹!");
}
ToString 재정의
클래스의 인스턴스 값을 적절히 표현하는 내용으로 재정의하는 것이 보통이다.
class Dog : Animal
{
string name;
public override string ToString()
{
return "제 이름은 " + this.name + "입니다."; //입력받은 강아지 이름에 따라서 다르게 출력.
}
}
Equals 재정의
public override bool Equals(object obj)
{
if(obj == null) //비교하려는 대상이 null인지 비교.
{
return false;
}
Animal animal = obj as Animal;
if(animal == null) //비교하려는 대상이 타입이 맞는지 비교.
{
return false;
}
return this.key == animal.key; //비교하려는 대상의 키값이 일치하는지 비교.
}
GetHashCode 재정의
GetHashCode는 해당 객체를 구별할 수 있는 key값을 반환하면 된다.
오버로드
이름만 같은 메서드가 매개변수의 수, 개별 매개변수 타입만 다르게 재정의하는 경우를 말한다.
메서드 오버로드
class Animal
{
public void PrintSound()
{
Console.WriteLine("소리를 낸다.");
}
public void PrintSound(Dog dog) //개를 받으면 멍멍소리를 낸다.
{
Console.WriteLine(dog.Sound);
}
public void PrintSound(Cat cat) //고양이를 받으면 야옹소리를 낸다.
{
Console.WriteLine(cat.Sound);
}
public void PrintSound(Whale whale) //고래를 받으면 고래소리를 낸다.
{
Console.WriteLine(whale.Sound);
}
}
class Dog : Animal
{
string sound = "멍멍";
public string Sound { get => sound; set => sound = value; }
}
class Cat : Animal
{
string sound = "야옹";
public string Sound { get => sound; set => sound = value; }
}
class Whale : Animal
{
string sound = "고래고래";
public string Sound { get => sound; set => sound = value; }
}
class OverloadEx
{
public static void Main()
{
Animal animal = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
Whale whale = new Whale();
animal.PrintSound(); //소리를 낸다. 출력
animal.PrintSound(dog); //멍멍 출력
animal.PrintSound(cat); //야옹 출력
animal.PrintSound(whale); //고래고래 출력 //메서드 이름은 같지만 매개변수에 따라서 다른 결과가 출력된다.
}
}
연산자 오버로드
연산자를 호출하는 객체의 의미에 맞게 재정의 할 수 있다.
public static Kilogram operator + (Kilogram op1, Kilogram op2)
{
return new Kilogram(op1.mass + op2.mass);
}
static void Main(string[] args)
{
Kilogram kg1 = new Kilogram(5);
Kilogram kg2 = new Kilogram(10);
Kilogram kg3 = kg1 + kg2; //연산자 오버로드로 Kilogram 내의 mass에 대한 더하기 수행.
Console.WriteLine(kg3.Mass); //결과는 5와 10을 더한 15가 나온다.
}
클래스 간의 형변환
implicit와 explicit 연산자를 이용해 형변환이 가능하다. 주로 무게, 길이, 환율 등 단위를 사용하는 프로그램에 유용하다.
class Won : Currency
{
public Won(decimal money) : base(money) { }
static public explicit operator Dollar(Won won) //원을 달러로 환전시 명시적 형변환만 가능.
{
return new Dollar(won.Money / 1154m); //1달러 = 1154원
}
}
class Dollar : Currency
{
public Dollar(decimal money) : base(money) { }
static public implicit operator Won(Dollar dollar) //달러를 원으로 환전시 암시적/명시적 형변환 가능.
{
return new Won(dollar.Money * 1154m); //1달러 = 1154원
}
}
class MoneyTest
{
public static void Main()
{
Dollar dollar1 = new Dollar(5); //5달러
Dollar dollar2 = new Dollar(10); //10달러
Won won1 = dollar1; //implicit로 선언하면 암시적 명시적 형변환 둘 다 가능.
Won won2 = (Won)dollar2;
Console.WriteLine(won1.Money);
Console.WriteLine(won2.Money);
Won won3 = new Won(1154);
Dollar dollar3 = (Dollar)won3; //explicit로 선언하면 명시적 형변환만 가능.
Console.WriteLine(dollar3.Money);
}
}
C# 클래스 확장
타입 유형 확장
중첩 클래스
중첩 클래스(Nested class)는 클래스가 클래스를 포함한 것이다. 포함 관계에 있는 클래스들의 구조를 표현하고, 포함된 클래스가 포함한 클래스 외의 클래스에서 정의되는 것을 막을 수 있다. 중첩 클래스는 접근 제한자를 생략하면 private으로 설정되서 외부에서 인스턴스 생성이 불가능하다.
class Car
{
class Engine
{
}
class Tire
{
}
Engine turboEngine;
Tire[] tire;
}
추상 클래스
abstract class Shape //Shape은 구체적인 도형이 아니므로 그려질 수 없다. 그래서 추상 클래스로 정의.
{
public abstract void Draw(); //추상 메서드는 자식 클래스에서 구현해야만 함
public void Move() { Console.WriteLine("움직인다."); }
}
class Line : Shape
{
public override void Draw() //구현하지 않으면 에러발생
{
Console.WriteLine("선을 그린다.");
}
}
class Circle : Shape
{
public override void Draw() //구현하지 않으면 에러발생
{
Console.WriteLine("원을 그린다.");
}
}
델리게이트
메서드 자체를 값으로 가지는 타입이다. 쉽게 말하면 메서드를 대신해서 호출하는 역할을 한다. 타입이기 때문에 다음의 특성을 가진다.
- 메서드의 반환값으로 델리게이트를 사용할 수 있다.
- 메서드의 인자로 델리게이트를 전달할 수 있다.
- 클래스의 멤버로 델리게이트를 정의할 수 있다.
델리게이트를 쓰는 주 이유는 콜백을 구현하기 위해서이다. 일을 할 수 있는 판을 짜놓고 일을 하다가 중간중간에 필요한 정보가 생기면 다른데에서 받아오는 것이 콜백의 의미이다. 다양한 타입의 객체들을 정렬할 때, 정렬 방법 함수만 바꿔가면서 델리게이트로 받아오면 다양한 상황에서의 처리를 하나의 코드로 할 수 있다.
델리게이트의 정의 형식은 다음과 같다.
접근제한자 delegate 반환타입 이름(매개변수) 의 형식을 가진다.
class DeleTest
{
public delegate int CalcDelegate(int a, int b); //반환형이 int이고, 2개의 int형 변수를 매개변수로 받는 메서드를 가리킬 수 있다.
public static void Calc(int a, int b, CalcDelegate dele) //델리게이트도 타입이기 때문에 매개변수로 넘길 수 있다.
{
Console.WriteLine(dele(a, b)); //매개변수 a,b를 대상으로 dele가 가리키는 연산을 수행한다.(콜백)
}
public static int plus(int a, int b) { Console.Write("더하기!"); return a + b; }
public static int minus(int a, int b) { Console.Write("빼기!"); return a - b; }
//정적/인스턴스 유형 메서드 모두 델리게이트가 가리킬 수 있다.
public int multiply(int a, int b) { return a * b; } //인스턴스 유형
public static void Main()
{
DeleTest dt = new DeleTest();
CalcDelegate Plus = new CalcDelegate(plus);
CalcDelegate Minus = DeleTest.minus; //C# 2.0부터는 간단하게 사용가능.
CalcDelegate Multiply = new CalcDelegate(dt.multiply); //인스턴스 메서드
CalcDelegate PlusAndMinus = Plus + Minus; //델리게이트 상대로 +,- 연산 가능.
Calc(3, 5, Plus);
Calc(7, 2, minus);
Calc(4, 5, dt.multiply);
Calc(1, 1, PlusAndMinus);
//PlusAndMinus에 Minus와 Plus가 같이 넘어가면서 dele(a,b)에서 각각 호출이 된다.
//하지만 Console.WriteLine 함수는 한 번만 실행되므로 뒤에 호출된 Plus값만 출력된다.
}
델리게이트를 사용한 정렬방식 구현의 예이다.
namespace ConsoleApplication1
{
delegate bool CompareDelegate(object arg1, object arg2); //비교 함수를 불러올 델리게이트
class SortObject
{
object[] things;
public SortObject() : this(null)
{
}
public SortObject(object[] things)
{
this.things = things;
}
public void Sort(CompareDelegate compareMethod) //델리게이트가 어떤 함수를 참조하냐에 따라서 정렬 기준이 달라진다.
{
object temp;
for (int i = 0; i < things.Length; i++)
{
int lowPos = i;
for (int j = i + 1; j < things.Length; j++)
{
if (compareMethod(things[j], things[lowPos]))
{
lowPos = j;
}
}
temp = things[i]; //스왑
things[i] = things[lowPos];
things[lowPos] = temp;
}
}
public void display()
{
Type type = things[0].GetType();
string typeName = type.Name;
if (typeName.Equals("Person")) //Person 객체 배열 출력
{
foreach (Person person in things)
{
Console.WriteLine(person.Name + " " + person.Age);
}
}
}
}
class Person
{
string name;
int age;
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public string Name { get => name; set => name = value; }
public int Age { get => age; set => age = value; }
}
class SortProgram
{
public static bool DescByName(object arg1, object arg2) //이름 내림차순 정렬
{
Person person1 = arg1 as Person;
Person person2 = arg2 as Person;
return person1.Name.CompareTo(person2.Name) > 0; //1의 이름이 앞 : -, 1의 이름이 뒤 : +, 같음 : =
}
public static bool DescByAge(object arg1, object arg2) //나이 내림차순 정렬
{
Person person1 = arg1 as Person;
Person person2 = arg2 as Person;
return person1.Age > person2.Age;
}
public static void Main()
{
Person[] personArray = new Person[] //비교를 실행할 사람 객체 배열
{
new Person("박도현", 24),
new Person("손의범", 25),
new Person("강윤구", 27),
new Person("검둥개", 4),
new Person("서태웅", 17)
};
SortObject sortObject = new SortObject(personArray);
sortObject.Sort(DescByName); //DescByName으로 하면 이름 내림차순으로 정렬.
sortObject.display();
}
}
}
인터페이스
구현 없는 메서드 선언이 포함되어 있다. C# 컴파일러가 인터페이스의 메서드를 가상메서드로 간주하고 있기 때문에 인터페이스에서 virtual 예약어와 자식클래스에서 override 예약어를 쓰지 못하게 막는다.
class Computer { }
interface IMonitor { void display(); }
interface IKeyboard { }
class NoteBook : Computer, IMonitor, IKeyboard
{
public void display()
{
//override 예약어는 쓰지 않아도 되지만, public은 반드시 명시해야함. notebook.display()와 같이 쓴다.
}
}
인터페이스와 다형성
interface IDrawingObject //인터페이스의 메서드는 가상 메서드이기 때문에 다형성의 특징이 적용된다.
{
void Draw();
}
class Line : IDrawingObject
{
public void Draw()
{
Console.WriteLine("선을그린다.");
}
}
class Circle : IDrawingObject
{
public void Draw()
{
Console.WriteLine("원을그린다.");
}
}
class Character { } //IDraingObject를 구현하지 않음.
class InterfaceTest
{
public static void Main()
{
IDrawingObject[] instances = new IDrawingObject[] { new Line(), new Circle() };
//인터페이스는 개별 객체로는 new 할 수 없지만 객체배열로는 가능하다.
IDrawingObject instance = new Line(); //인터페이스를 직접 new 하지 않고 이런식으로 활용 가능.
instance.Draw(); //선을그린다 출력.
object[] compareArr = new object[] { new Line(), new Circle(), new Character()};
foreach (object obj in compareArr) //인터페이스 자체를 구현하는지 여부로 클래스 구분 가능.
{
if (obj is IDrawingObject) Console.WriteLine("인터페이스를 구현함.");
}
}
}
인터페이스와 콜백
인터페이스로도 콜백을 구현할 수 있다. 델리게이트와 차이는 다음과 같다.
- 델리게이트는 각 메서드 시그니쳐에 따라 다르게 정의해야 하지만, 인터페이스는 하나의 타입에서 여러 개의 메서드를 정의 가능하다.
- 델리게이트는 한 번에 여러 개의 메서드를 담을 수 있어서 한 번에 여러 개의 메서드 호출이 가능하다.
class AgeCompare : IComparer
{
public int Compare(object x, object y) //원래 Compare함수는 오름차순 정렬이지만, 내림차순 정렬로 재구현
{
Person person1 = x as Person;
Person person2 = y as Person;
if (person1.Age > person2.Age) return -1; //앞에가 작으면 1, 같으면 0, 크면 -1;
else if (person1.Age == person2.Age) return 0;
else return 1;
}
}
Array.Sort(PersonArray, New AgeCompare()); //PersonArray가 나이 내림차순 기준으로 정렬된다.
IEnumerable 인터페이스
자바의 Iterator와 비슷하다. IEnumerable 인터페이스를 구현하고 있는 클래스는 열거자를 통해 get(), MoveNext() 등의 메서드를 쓸 수 있다.
foreach의 in 다음에 오는 객체가 IEnumerable 인터페이스를 구현하고 있다면 foreach 사용이 가능하다.
느슨한 결합
interface IPower
{
void TurnOn();
}
class Monitor : IPower
{
public void TurnOn()
{
Console.WriteLine("Monitor: TurnOn");
}
}
class Computer : IPower
{
public void TurnOn()
{
Console.WriteLine("Computer: TurnOn");
}
}
class Switch
{
public void PowerOn(IPower machine) //매개변수를 인터페이스로 받으면 클래스를 매개변수로 받는 것보다 느슨한 결합을 달성가능하다.
{ //클래스를 매개변수로 받으면 매개변수가 바뀌었을 때 Computer를 Monitor로 바꿔도 Switch에 대한 코드를 수정해야한다.(강한 결합)
machine.TurnOn();
}
}
구조체
사용자 정의 형식이지만 값 형식을 가진다. 클래스와 차이는 다음과 같다.
- 인스턴스 생성을 new로 해도 되고, 안 해도 된다. (new로 하면 구조체 내 모든 속성값 0으로 할당.)
- 기본 생성자는 명시적으로 정의할 수 없다.
- 매개변수를 갖는 생성자를 정의해도 기본 생성자가 C# 컴파일러에 의해 자동으로 포함된다.
- 매개변수를 받는 생성자의 경우, 반드시 해당 코드 내에서 구조체의 모든 필드에 값을 할당해야 한다.
값 형식에 속하는 모든 타입은 기본적으로 0으로 초기화된다. 하지만 컴파일러는 개발자가 직접 초기화하지 않은 상태의 변수를 사용하는 것은 오류라고 판단하기 때문에 명시적으로 초기화를 해줘야한다.
struct Vector
{
int x;
int y;
public Vector() //매개변수 없는 생성자 정의 불가. (에러)
{
}
public Vector(int x) //모든 필드에 값을 할당해야 한다.(에러)
{
this.x = x;
}
public Vector(int x, int y) //매개변수 있는 생성자는 정의 가능.
{
this.x = x;
this.y = y;
}
}
class Program
{
Vector vec = new Vector(); //속성값 0으로 초기화.
Vector vec2; //이것도 선언 가능.
var v = new Vector() { x = 1, y = 2 }; //이런식으로 간단히 값을 저장해두고 쓴다.
}
깊은 복사와 얕은 복사
깊은 복사와 얕은 복사의 차이는 대입할 때 나타난다. 깊은 복사는 값 타입의 경우에 메모리가 똑같이 복사되서 값의 변경이 있더라도 서로 영향을 미치지 않는다. 반면 얕은 복사는 참조 타입의 경우라 참조 주소만을 복사한다. 그래서 값의 변경이 일어나면 다른 곳에서도 원치 않는 값의 변경이 있을 수 있다.
깊은 복사는 메모리를 복사하기 때문에 그런 경우를 막을 수 있지만, 크기가 큰 메모리 같은 경우엔 그 크기 단위로 복사하기 때문에 부담을 줄 수 있다.
반면에 얕은 복사는 참조 주소값만 복사하면 되므로 그 부분에선 더 좋은 성능을 보여준다.
구조체와 클래스를 선택하는 기준
- 일반적으로 모든 사용자 정의 타입은 클래스로 구현한다.
- 깊은/얕은 복사의 차이가 민감한 타입은 선택적으로 구조체로 구현한다.
- 참조 형식은 GC에 의해 관리받게 된다. 따라서 참조 형식을 사용하는 경우 GC에 부담이 되는데 이런 부하를 피해야 하는 경우에는 구조체를 선택한다.
ref 예약어
- 값에 의한 호출 : 메서드의 인자 전달 시 변수의 스택 값이 복사되어 전달되는 것을 말한다.
- 참조에 의한 호출 : 해당 변수의 스택 값을 담고 있는 주소 자체가 전달되는 것을 말한다.
그래서 참조에 의한 호출과 얕은 복사가 비슷해 보이지만 차이점이 있다. 얕은 복사는 스택 내에 새로운 공간을 할당하고 그 공간도 같은 주소를 가리키게 된다. 하지만 참조에 의한 호출은 스택 내에 새로운 공간을 만들지 않고 동일한 공간을 참조한다는 점이 다르다. 그림을 보면 차이점이 확실히 드러난다.
다음은 얕은 복사와 참조에 의한 호출을 비교한 그림이다.
ref 인자로 전달하려는 변수는 new나 null이나 그 외의 어느 값이든 초기화가 되어야 전달이 가능하다.
out 예약어
- out으로 지정된 인자에 넘길 변수는 초기화하지 않아도 된다. 초기화돼 있어도 out 인자를 받는 메서드에서는 그 값을 사용할 수 없다.
- out으로 지정된 인자를 받는 메서드는 반드시 변수에 값을 넣어서 반환해야 한다.
class OutTest
{
static bool Divide(int n1, int n2, out int result)
{
if (n2 == 0)
{
result = 0; //out으로 선언한 result에 대한 초기화가 이루어지지 않으면 에러발생.
return false;
}
result = n1 / n2;
return true;
}
public static void Main()
{
int divideResult = 5;
if(OutTest.Divide(10, 0, out divideResult) == true)
{
Console.WriteLine(divideResult);
}
else
{
Console.WriteLine("0으로나눌수없어!");
}
}
}
결국 ref 예약어는 기존 변수를 메서드 내에서 수정하려 할 때 사용하고, out 예약어는 메서드 내에서 수정된 값을 반환하려 할 때 사용한다.
열거형
값 형식으로 byte, sbyte, short, ushort, int, uint, long, ulong만을 상속받아 정의할 수 있는 제한된 사용자 정의 타입이다. 타입을 지정하지 않으면 int 형으로 지정된다. 코드의 가독성을 높이거나, 오타로 인한 오류를 줄여줄 수 있다.
사용 예
class EnumTest
{
[Flags]
enum Status : int
{
Menu = 1, GameStart = 2, GameOver = 4, Ranking = 8, Option = 16
//OR연산을 했을 시 값이 다른 값에 먹히지 않으려면 2의 배수로 증가시켜야 함.
//미 정의시 0부터 시작해서 1씩 증가.
}
public static void Main()
{
Status status = Status.GameOver;
Console.WriteLine(status); //출력값은 GameOver.
Console.WriteLine((int)status); //출력값은 2.
Status status2 = Status.GameOver | Status.GameStart | Status.Ranking | Status.Option;
if (status2.HasFlag(Status.GameOver)) //포함여부 확인가능.
{
Console.WriteLine("GameOver를 포함하고 있습니다.");
}
Console.WriteLine(status2); //OR연산을 하고 출력하면 합계 값이 나온다. [Flags] 선언시 요소 이름으로 출력
}
}
멤버 유형 확장
읽기 전용 필드
readonly 예약어를 사용하면 클래스 내부에서 읽기만 가능하고 초기화도 멤버 선언이나 생성자에서만 가능하다. 내부적으로 불변 상태를 보장하기 위하여 쓴다.
public class ReadonlyEx
{
readonly int num = 10; //멤버 선언에서 초기화 가능.
public ReadonlyEx()
{
this.num = 3; //생성자에서 초기화 가능. 둘 다 했을 시 생성자에서 덮어쓰기 함.
}
public void test()
{
//this.num = 10; 그 외의 장소에서는 초기화하면 에러.
}
public static void Main()
{
ReadonlyEx ex = new ReadonlyEx();
Console.WriteLine(ex.num);
}
}
상수
리터럴에 식별자를 붙인것이다. 리터럴의 재사용과 수정을 용이하게 해준다.
[접근제한자] const [상수타입] [식별자] = 값; 으로 사용한다.
다음은 상수의 몇 가지 특징이다.
- 상수는 static 예약어가 허용되지 않는다. (의미상으론 static에 해당한다.)
- 기본 자료형에 대해서만 상수 정의가 가능하다.
- 반드시 상수 정의와 함께 값을 대입해야 한다. 생성자에서 접근 불가.
- 컴파일할 때 소스코드에 값이 치환되는 방식으로 구현된다.
이벤트
정형화 된 콜백 패턴을 구현할 때 event 예약어를 쓰면 코드를 줄일 수 있다. 이벤트의 특징은 다음과 같다.
- 클래스에서 이벤트를 제공한다.
- 외부에서 자유롭게 해당 이벤트를 구독하거나 해지하는 것이 가능하다.
- 외부에서 구독/해지는 가능하지만 이벤트 발생은 오직 내부에서만 가능하다.
- 이벤트의 첫 번째 인자는 이벤트를 발생시킨 타입의 인스턴스다.
- 이벤트의 두 번째 인자는 해당 이벤트에 속한 의미 있는 값이 제공된다.
이벤트는 GUI를 제공하는 응용 프로그램에서 일반적으로 사용된다. 윈도우에 포함된 버튼이 있고 버튼을 눌렀을 때 해야하는 작업이 있다면, 버튼 클래스 제작자는 Click 이라는 이벤트를 구현해두고, 버튼을 이용하는 개발자는 Click 이벤트를 구독하는 메서드 내에서 작업 수행 코드를 작성하면 된다.
이벤트 선언 방법
[접근제한자] delegate [리턴타입] [델리게이트명] [파라미터];
[접근제한자] event [델리게이트명] [이벤트명];
namespace ConsoleApplication1
{
//class CallbackArg { } //유연성을 위해 상속 구조를 만들어서 실행 메서드 내에서 다시 형변환 하는 방식.
class PrimeCallbackArgs : EventArgs //CallbackArg대신 닷넷에서 제공하는 EventArgs 상속.
{
public int prime;
public PrimeCallbackArgs(int prime)
{
this.prime = prime;
}
}
class PrimeGenerator //발행자 클래스.
{
//public delegate void PrimeDelegate(object sender, EventArgs arg);
//public event PrimeDelegate PrimeGenerated; //델리게이트를 정의하고 그 델리게이트를 위한 이벤트 정의가능.
public event EventHandler PrimeGenerated;
//닷넷에서 제공하는 EventHandler를 사용한 경우.
//콜백 메서드 추가/삭제하는 메서드. 이벤트를 사용하면 더 간결하게 쓸 수 있다.
//public void AddDelegate(PrimeDelegate callback)
//{
// callbacks = Delegate.Combine(callbacks, callback) as PrimeDelegate;
//}
//public void RemoveDelegate(PrimeDelegate callback)
//{
// callbacks = Delegate.Remove(callbacks, callback) as PrimeDelegate;
//}
public void Run(int limit)
{
for (int i = 2; i <= limit; i++)
{
if (IsPrime(i) == true && PrimeGenerated != null)
//이벤트 발생 조건과(여기에선 소수여부) 이벤트를 구독하는 구독자가 있는지 검사.
{
PrimeGenerated(this, new PrimeCallbackArgs(i));
//발행자 내부에서 이벤트 발생.
//System.EventHandler는 두 개의 매개변수를 요구.
//이벤트의 첫 번째 인자는 이벤트를 발생시킨 타입의 인스턴스.
//이벤트의 두 번째 인자는 해당 이벤트에 속한 의미 있는 값.(여기에선 소수값)
}
}
}
private bool IsPrime(int candidate)
{
if ((candidate & 1) == 0)
{
return candidate == 2;
}
for (int i = 3; (i * i) <= candidate; i += 2)
{
if ((candidate % i) == 0) return false;
}
return candidate != 1;
}
}
class Program //구독자 클래스
{
static void PrintPrime(object sender, EventArgs arg)
{
Console.Write((arg as PrimeCallbackArgs).prime + ", ");
}
static int sum;
static void SumPrime(object sender, EventArgs arg)
{
sum += (arg as PrimeCallbackArgs).prime;
}
static void Main()
{
PrimeGenerator gen = new PrimeGenerator(); //이벤트 발행자 객체 행성. 이벤트 구독자는 현재 객체.
//gen.PrimeGenerated(this, new PrimeCallbackArgs(i)); //이벤트 선언 클래스 외에서 호출 시 에러.
//PrimeGenerator.PrimeDelegate callprint = PrintPrime;
//gen.AddDelegate(callprint);
//PrimeGenerator.PrimeDelegate callsum = SumPrime;
//gen.AddDelegate(callsum);
//이벤트를 쓰면 아래와 같이 코드를 줄일 수 있다.
//외부에서 구독/해지 가능
gen.PrimeGenerated += PrintPrime; //PrintPrime 메서드의 이벤트 등록
gen.PrimeGenerated += SumPrime; //SumPrime 메서드의 이벤트 등록
gen.Run(10);
Console.WriteLine();
Console.WriteLine(sum);
//gen.RemoveDelegate(callsum);
gen.PrimeGenerated -= SumPrime; //SumPrime 메서드의 이벤트 해지
gen.Run(15);
}
}
}
질문사항
- 스택 영역의 사이즈는 어떻게 결정되는가?
스택 영역은 스레드 당 1개씩 생성되며, 기본 크기는 1MB이다. 하지만 1MB를 다 commit 한 상태는 아니고 reserve 상태에서 4KB씩 commit 한다.
* 생성자 내에서 exception이 발생되면 객체에는 어떤 값이 리턴되는가? 실제 코드로 실험해보려 했지만 객체가 해제되는 적당한 예제를 만들기가 쉽지 않았다. 아래는 MSDN 검색 결과이다. 생성자에서 예외가 발생했을 때, 생성자가 호출돼서 객체 자체의 메모리는 이미 할당된 상태입니다. 따라서 컴파일러는 예외가 발생 된 후 객체가 차지하는 메모리를 자동으로 해제합니다. (MSDN)
* C#에서 전역 변수는 언제 쓰는가? C#은 일부 다른 언어와 달리 전역 변수 또는 메서드가 없습니다.(MSDN)
* 추상클래스와 인터페이스는 프로퍼티를 가질 수 있는가? 추상클래스는 가질 수 있고, 인터페이스는 가질 수 없다.
* 추상클래스와 인터페이스의 차이는? 추상클래스와 인터페이스는 설계 관점에서 봤을 때, 추상클래스는 부모클래스를 알고 써야한다는 제약이 있으나 코드를 줄일 수 있고, 구조의 변화 가능성이 적을 때 쓰는게 좋다. 인터페이스는 매개변수로 넘기기 편해서 유연한 설계가 가능하다. 추상클래스를 사용하는 것보다는 인터페이스를 사용하는 것이 구조의 변화를 좀 더 수용할 수 있다.
* sealed 예약어를 쓰는 이유는? 가상 함수를 호출하게 되면 CLR은 런타임에 호출 변수가 실제로 참조하는 타입을 확인해서 그에 맞는 메서드를 호출한다. 하지만 sealed 예약어를 쓰면 CLR은 더이상 메서드를 찾을 클래스가 없다는 것을 알려주기 때문에 작업 속도가 향상된다.
* object 객체를 new 써서 생성할 수 있는가? 가능하다.
- 배열을 GetType해서 IsClass를 출력하면 결과값이 어떻게 나오는가?
Type type = arr.GetType();
Console.WriteLine(type.IsClass); //배열은 IsClass 했을 때 true
type = enumTest.GetType();
Console.WriteLine(type.IsClass); //열거형은 IsClass 했을 때 false
Vector v = new Vector() { x = 1, y = 2 }; //이렇게 간단 초기화 가능
type = v.GetType();
Console.WriteLine(type.IsClass); //구조체는 IsClass 했을 때 false
- 객체 사이에 == 연산자를 사용하면?
class EqualOpTest
{
public static void Main()
{
string s1 = "검둥개";
string s2 = "검둥개";
string s3 = s1;
if (s1.Equals(s2)) Console.WriteLine("Equals1같음."); //출력
if (s1.Equals(s3)) Console.WriteLine("Equals2같음."); //출력
if (s1 == s2) Console.WriteLine("==같음1"); //출력 //string의 ==는 equals를 호출하기 때문에 equals와 기능적인 차이는 없다.
if (s1 == s3) Console.WriteLine("==같음2"); //출력
Dog dog1 = new Dog(10);
Dog dog2 = new Dog(10);
Dog dog3 = dog1;
if (dog1.Equals(dog2)) Console.WriteLine("dog1,dog2 equal."); //출력X //스택값이 다르기 때문에
if (dog1.Equals(dog3)) Console.WriteLine("dog1,dog3 equal."); //출력 //스택값이 같으므로
if (dog1 == dog2) Console.WriteLine("dog1,dog2 ==."); //출력X
if (dog1 == dog3) Console.WriteLine("dog1,dog3 ==."); //출력O
// == 연산자는 같은 형의 기본형에 대한 비교를 위한 연산자로서 양쪽의 자료형이 다르면 컴파일 단계에서 연산자 에러가 난다.
// Equals 메서드는 2개의 기본형이나 객체에 대한 비교를 수행하는 형으로서 2개의 형이 다르면 비교시 에러가 나지 않고,
// 형이 동일할 경우 값이 일치 여부까지 비교한다.
}
}
class Dog
{
int age;
public Dog(int age)
{
this.age = age;
}
public override bool Equals(object obj)
{
Dog d = obj as Dog;
if ((object)d == null)
{
return false;
}
return base.Equals(obj) && age == d.age;
}
public int Age { get => age; set => age = value; }
}
- 같은 값을 가지는 short와 int에 Equals() 메서드를 사용하면 결과값이 어떻게 나오는가?
int a = 256;
short b = 256;
Console.WriteLine(a.Equals(b)); //True 출력.
Console.WriteLine(b.Equals(a)); //False 출력.
//내부적으로 as 연산자로 형변환을 하기 때문에
//형변환이 되는 short -> int는 형변환 후 값을 비교해서 값이 같기 때문에 true.
//형변환이 되지 않는 int -> short는 as 연산 후 null 값을 리턴하기 때문에 false.
- switch case 문에서 case에 문자열이 올 수 있는가?
class SwitchString
{
public static void Main()
{
string input = Console.ReadLine();
switch (input) //문자열도 가능하다.
{
case "abc":
Console.WriteLine("abc가 입력되었습니다.");
break;
case "123":
Console.WriteLine("123이 입력되었습니다.");
break;
default:
Console.WriteLine("그 외 나머지가 입력되었습니다.");
break;
}
}
}
-
readonly와 const의 차이는?
C# 에서 const는 컴파일 상수이며, readonly는 런타임 상수이다. const는 내장자료형(정수형,실수형, enum, string)에 대해서만 쓸 수 있으며, 변수 선언과 동시에 초기화를 해야한다. readonly는 모든 자료형에 사용 할 수 있으며, 변수 선언과 동시에 초기화하거나 생성자에서 초기화 해야한다.const는 컴파일 상수이기 때문에 const로 선언된 변수가 사용되면 변수에 대한 참조가 아닌, 실제 상수로 치환되어 사용 된다. 이렇게 const로 선언되면 수행 성능이 좋아지지만 const 변수 변경 시, 다시 컴파일을 해야한다. 그렇기 때문에 const는 ‘이후에 바뀔 가능성이 없는 상수’에 주로 쓰이고, readonly는 그 이외의 상수에 주로 사용한다.