객체 지향 문법
객체지향
현실 세계의 모든 것들을 '속성'과 '행위'로 정의한다.
클래스
기본 자료형 타입으로는 현실 세계의 모든 것들을 표현하기 힘들기 때문에, 개념이 비슷한 것들을 일반화하여 표현하기 위해 직접 타입을 정의한 것이다.
클래스로 정의된 타입은 모두 참조형으로 분류되므로 사용하려면 new 연산자로 메로리 할당을 해야한다.
필드
클래스의 속성을 의미한다. 또는 객체에 속한 변수이다. 멤버 변수라고도 한다.
메서드
클래스의 행위를 의미한다. 또는 객체에 속한 함수이다. 서브루틴, 프로시저라고도 한다. 메서드를 쓰는 이유는 다음과 같다.
- 중복 코드 제거 : 중복된 코드는 메서드로 묶어서 편하게 이용할 수 있다.
- 코드 추상화 : 메서드의 내부를 몰라도 입출력 인자의 용도만 안다면 사용 가능하다.
생성자
객체가 생성되는 시점에 자동으로 호촐되는 메서드. 반환타입을 명시하지 않는 점을 제외하면 일반 메서드를 정의하는 방법을 따른다.
소멸자
객체가 제거되는 시점에 자동으로 호출되는 메서드. C#은 C++와 달리 delete라는 예약어가 없고, GC가 호출돼야 소멸자가 호출된다. 그래서 정확히 언제 소멸자가 호출되는지 알 수 없다. 소멸자의 사용은 권장되지 않고, 주로 IDisposable을 구현하여 Dispose()를 사용하는 방법이 권장된다.
다음은 생성자와 소멸자의 예시이다.
class Book
{
public Book() //생성자
{
}
~Book() //소멸자
{
// 자원 해제
}
}
정적 멤버, 인스턴스 멤버
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() //이 메소드를 쓰려면 Dog.KingDog.Bark() 이런식으로 호출 가능하다.
{
Console.WriteLine("첫번째 인자로 멍멍이 입력되었습니다. 멍멍!");
}
public static void Main(string[] args)
{
if (args[0].Equals("멍멍"))
{
Dog.KingDog.Bark();
}
}
다음과 같은 실행 결과를 확인할 수 있다.
정적 생성자
정적 멤버를 초기화하는 기능을 한다. 클래스에 단 한개만 존재 할 수 있다. 최초로 접근하는 시점에 우선적으로 단 한 번만 실행된다.
public static Dog KingDog = new Dog("왕개"); //위 아래는 같은 코드가 된다.
public static Dog KingDog = new Dog();
static Dog()
{
KingDog = new Dog("왕개");
}
네임스페이스 , 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, class 내부의 멤버들은 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에서 name을 눌러 자동생성 가능.
{
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은 Dog다."); //변환이 안되므로 실행 안됨
}
}
}
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); //배열의 차수 1
Console.WriteLine(intArray.Length); //배열의 길이
foreach (int num in intArray)
{
Console.Write(" "+intArray.GetValue(i++)); //정렬 전 배열 출력
}
Array.Sort(intArray); //배열 정렬
i = 0;
foreach (int num in intArray)
{
Console.Write(" " + intArray.GetValue(i++)); //정렬 후 배열 출력
}
int[] copyArray = new int[intArray.Length];
Array.Copy(intArray, copyArray, 5); //intArray에서 copyArray로 5개만 복사
i = 0;
foreach (int num in copyArray)
{
Console.Write(" " + copyArray.GetValue(i++)); //복사 후 배열 출력
}
}
}
this
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 Sound()
{
Console.WriteLine("소리를 낸다.");
}
public void Sound(Cat cat)
{
Console.WriteLine("야옹");
}
public void Sound(Dog dog)
{
Console.WriteLine("멍멍");
}
public void Sound(Whale whale)
{
Console.WriteLine("고래고래");
}
}
animal.Sound(); //소리를 낸다 출력. //메서드의 이름은 같지만 매개변수에 따라서 다른 결과 출력.
animal.Sound(dog); //멍멍 출력.
animal.Sound(cat); //야옹 출력.
animal.Sound(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 = Minus + Plus; //델리게이트 상대로 +,- 연산 가능.
Calc(3, 5, Plus);
Calc(7, 2, minus);
Calc(4, 5, dt.multiply);
Calc(1, 1, PlusAndMinus);
//PlusAndMius에 Minus와 Plus가 같이 넘어가면서 dele(a,b)에서 각각 호출이 된다.
//하지만 Console.WriteLine 함수는 한 번만 실행되므로 뒤에 호출된 Plus값만 출력된다.
}
콜백을 구현하기 위해서 주로 델리게이트를 사용한다.