참고로 c#은 console과 winform 둘 다 개발이 가능.
전체적으로 한 번 훑어보자.
일반적으로 C와 비슷함. 다만 다음이 있음.
- type testing operator
is
: 호환 가능한가? (inheritance)as
: 해당 type으로 변환obj as string
뭐 이렇게
선언 및 초기화는 각각 다음과 같음
int [] arr1;
- 1차원 배열 선언
int[] arr1 = new int[10]; arr[0] = 1;
이렇게 하거나int[] arr1 = {0, 1, 2, 3};
뭐 이렇게 할 수가 있다.
short[,] arr2;
- 2차원 배열 선언. 사용은 다음과 같음.
short[,] arr2 = new short[2, 10];
int[,,] arrarr;
이따위로도 가능
- 2차원 배열 선언. 사용은 다음과 같음.
int[][] arr3;
- 가변 배열. 사용은 다음과 같음.
int[][] arr3 = new int [3][];
arr3[0] = new int[5];
- 2차원 배열과는 다른것이... 각각이 서로 다른 길이를 가질 수 있다는 것.
- 가변 배열. 사용은 다음과 같음.
cs에서 string은 Object이다. 대부분 마찬가지기는 하지만...
참고로 string
은 String
객체에 대한 alias이다. 리터럴은 ""
- c#은 특이하게 indexer, override operator가 가능하다.
- Destructor도 있음. 진행하며 보겠지만
~<class name>
으로 정의 가능
getter/setter 정의하는 애
class A {
private int data = 0;
public int Data { // 당연히 반드시 public일 필요는 없음. 그냥 얘도 멤버임
// 반드시 둘 다 정의할 필요는 없다.
get { return data; }
set { data = value; } // value: setter value
}
}
/* ... */
A a = new A();
Console.WirteLine(a.Data); // 0
a.Data = 10;
Console.WriteLine(a.Data); // 10
연산자 재정의. 말 그대로 연산자의 기능을 재정의.
class A {
private int data = 0;
public int Data {
get { return data; }
set { data = value; }
}
// public static 이어야 함
public static A operator + (A a1, A a2) {
A a = new A();
a.Data = a1.Data + a2.Data;
return a;
}
}
/* ... */
A a1 = new A();
A a2 = new A();
a1.Data = 10;
a2.Data = 20;
a3 = a1 + a2;
Console.WriteLine(a3.Data); // 30
구조를 잘 알아두자. public static <class name> operator <operator>
이거임.
웬만한 애들은 다 overload 가능한데, 반환은 연산자 의미에 걸맞게 해줘야함. 가령, ==
이걸 overload 한다고 했을 때 반환이 bool
이어야겠지.
function pointer. 그냥 callback이라고 생각하는게 편함. 실제로도 그렇고.
delegate string Dele (int a);
public class A {
public string print (int a) {
return $"value is {a}";
}
}
/* ... */
A a = new A();
Dele d = new Dele(a.print); // print 메서드를 Dele 델리게이트 사용해 d로 가리키도록 함
Console.WriteLine(a.print(1)); // value is 1
Console.WriteLine(d(1)) // value is 1
보이다싶이 다음의 조건이 맞아야 한다.
- return type, arguments의 length와 type이 모두 동일해야 한다.
- 이것만 동일하다면 가리키는 함수가 static이든 instance이든 상관없음.
어렵게 생각하지는 말자. 참고로 lambda와 함께 Dele d = new Dele(a => $"value is {a}")
이렇게도 되지 않을까? 해보지는 않아서 잘 모르겠다.
System.Threading
을 using 해야만 한다.
ThreadStart
델리게이트에 메서드 연결한 뒤, 이 ThreadStart
델리게이트 객체를 Thread
클래스 생성자로 보내주면 되는거.
using System;
using System.Threading;
class T {
static void Body () {
Console.WriteLine("in threading...");
}
public static void Main () {
ThreadStart ts = new ThreadStart(Body);
Thread t = new Thread(ts);
/* ... */
}
}
다음과 같이 정의 가능
class Stack<T> {
public void Method<E> () { /* ... */ }
/* ... */
}
이따위로 메서드에도 제네릭 사용이 가능하다는 것을 알아두자.
중간 코드가 있다. 확장자가 *.il
이거임.
plt에서도 봤던 것들
다음으로 구성되어있다.
- keyword(지정어):
abstract
,asn
, ...@
붙이면 지정어(변수명)로 사용이 가능하다.@int
이렇게
- operator(연산자):
+
,-
, ... - delimiter(구분자):
.
,:
, ... - identifier(명칭):
sum
,ptr
, ...- 변수이름 이런거 말하는거
- 반드시 문자로 시작해야하고, 특수문자는
_
만 사용가능- 맨앞에는
@
사용가능한데, 지정어 덮어씌울때만임. 그외에는 불가능
- 맨앞에는
- literal(리터럴)
여기서 keyword는 개많이있으며 여기서 하나 나올 것 같긴 한데 아..
참고로 놀랍게도 다음이 정상실행된다.
using System;
public class Program
{
public static void Main()
{
double π = 3.1415926535897;
Console.WriteLine(π);
}
}
유니코드 쓰기에 되는거. 근데 여기서 pi 기호를 문자취급하나봄.
- c#에서는 8진수 지원안하고, 16진수는 앞에
0X
를 붙인다. - true, false가 1, 0을 의미하지 않음
- 다음과 같이
@
을 이용해\
표현 가능"hello \\t world";
원래는 이랬는데@"hello \t world";
이렇게 사용할 수 있다.- 둘 다 hello world 를 표현함
///
이거 세 개 쓴거는 docs 작성용이라고 한다. XML 사용한다고 함.
뭐 개많다.
-
value type
- number
- string
- boolean
- enum
- structure
-
ref type
- calss
- interface
- delegate
- array
웃긴게 CTS type이라고 다음과 같이도 선언이 가능하다는거
System.Int32 x;
int x;
Convert 하면서 Int32
뭐 이런이름들 종종 봤을텐데, 이게 이거다.
자료형의 크기도 나올까? 그러지는 않을 것 같긴 한데...
c#의 pointer 연산은 반드시 다음과 같은 unsafe
블록 안에서 정의되어 있어야 하며,
class Program {
unsafe public static void swap (int *px, int *py) { // unsafe 키워드가 포함된 블록
int tmp = *px;
*px = *py;
*py = tmp;
}
public static void Main (string[] args) {
int x = 1, y = 2;
Console.WriteLine($"BEFORE >> x: {x}, y: {y}"); // BEFORE >> x: 1, y: 2
unsafe {
swap(&x, &y);
}
Console.WriteLine($"AFTER >> x: {x}, y: {y}"); // AFTER >> x: 2, y: 1
}
}
컴파일 시 /unsafe
옵션을 추가해주어야만 한다.
csc /unsafe Program.cs
- 보수는
~x
와 같이~
키워드를 사용 - XOR는
^x
와 같이^
키워드를 사용 - overflow 검사 또는 무시에 따라 연산자를 지정할 수도 있다.
- overflow 검사 연산자는
checked
- overflow 무시 연산자는
unchecked
- overflow 검사 연산자는
형 변환 시 범위가 작은 타입에서 큰 타입으로는 묵시적으로 변환되나, 범위가 큰 타입에서 작은 타입으로는 명시적으로 해 줘야만 한다. 이 때 cast operator를 사용하겠지.
참고로, bool
은 다른 타입으로의 conversion이 불가능하다.
- Boxing
- value를 ref로 변환하는 것. 묵시적으로 됨
int
toobject
- Unboxing
- ref를 value로 변환하는 것. 반드시 명시적으로
object
toint
unboxing 시 원래의 타입으로만 unboxing이 가능하다. 그 외에는 exception
int foo = 10;
object bar = foo; // boxing (implicit)
int i1 = bar; // ERROR
int i2 = (int) bar; // unboxing (explicit)
double d = (double) bar; // ERROR
대충 지금까지 봤던것과 겹치는게 많다.
foreach
문이 처음 등장하기는 하는데, 뭐 이름에서 뭐하는애인지는 잘 나오니까...foreach (<type> <name> in <array>) { ... }
goto
는 다음이 불가- 블록 안으로 비집고 들어갈 수 없다.
- 메서드 밖으로 벗어날 수 없다.
- finally 밖으로 벗어날 수 없다.
다음 두 형태가 있음.
checked {
/* overflow 발생 여부 확인 문장 */
}
checked ( /* overflow 발생 여부 확인 수식 */ )
만약 위의 수식 또는 문장에서 overflow 발생 시, System.OverflowException
예외가 발생된다. 무시하려면 unchecked
키워드 사용해서 똑같이 쓰면 됨.
- Input
Console.Read()
: 문자 하나를 읽어 정수형으로 반환Console.ReadLine()
: 하나의 line을 읽어 string으로 반환
- Output
Console.Write()
: 출력Console.WriteLine()
: 출력 후 line break
printf
와 비슷한 것을 사용할 수 있다. 포맷은 다음과 같음
{N[,W][:format]}
N
: 위치 지정(0부터 시작)W
: 폭 지정(-
붙으면 좌측정렬)format
: 형식 지정 문자
여기서 형식 지정 문자는 다음과 같다.
지정자 | 의미 |
---|---|
c |
통화 |
d |
10진수 |
e |
지수 |
f |
고정 소수점 |
g |
고정 소수점 또는 지수. 이 둘 중에서 더 간략한 형태를 선택함 |
n |
자릿수 구분(, ) 들어간 10진수 |
p |
백분율. 여기서 % 기호 들어간다. |
r |
좀 더 길게 뽑는 애인듯? 원 형태로 출력하는 것이랜다. |
x |
16진수 |
여기서 지정자는 대소문자 포함된거다. 구분안함.
public class Program
{
public static void Main()
{
double d = Math.PI;
Console.WriteLine("1) " + d);
Console.WriteLine("2) {0}", d);
Console.WriteLine("3) {0:C}", d);
Console.WriteLine("4) {0:E}", d);
Console.WriteLine("5) {0:F}", d);
Console.WriteLine("6) {0:G}", d);
Console.WriteLine("7) {0:P}", d);
Console.WriteLine("8) {0:R}", d);
Console.WriteLine("9) {0:X}", 255);
}
}
/* OUTPUT
1) 3.14159265358979
2) 3.14159265358979
3) $3.14
4) 3.141593E+000
5) 3.14
6) 3.14159265358979
7) 314.16 %
8) 3.1415926535897931
9) FF
*/
자료 추상화의 한 방법
대부분 본 애들.
- public
- internal
- 같은 네임스페이스 안에서만 사용이 가능
- static
- abstract
- sealed
- 아래에서 다룸
- protected
- 파생 클래스에서만 사용이 가능
protected internal
같이 두 개 쓰면 같은 네임스페이스 또는 파생 클래스 둘 다에서 사용이 가능해짐
- private
- 수정자 생략 시 적용되는 수정자
- new
- 중첩 클래스에서 사용되며, parent(base) 클래스의 멤버를 숨김 (override)
- 좀 아래에서 다룸
- readonly
- 읽기전용
- runtime 시 값이 결정됨
- const
- 읽기전용
- compile 시 값이 결정됨
메서드의 수정자들이랜다.
- public, protected, internal, private
- 뭐 이런 접근 수정자 있고
- static
- abstract, extern
- 둘 다 추상메서드이긴 한데, 차이점은 다음과 같음.
- abstract: 메서드가 하위 클래스에 정의됨
- extern: 메서드가 외부에 정의됨
- new, virtual, override, sealed
- 뒤에서 봄. 근데 대충 의미는 해석이 가능하고
이 둘이 뭘 의미하는지는 알 것이고, 이거에 따라서도 수정자가 있더라.
- ref: 매개변수 전달 시 반드시 초기화 되어야 함
- out: 매개변수 전달 시 지멋대로 가능
class Program {
static void swap (ref int x, ref int y) {
int tmp = x;
x = y;
y = tmp;
}
public statc void Main (string[] args) {
int x = 1, y = 2;
Console.WriteLine($"x: {x}, y: {y}"); // x: 1, y: 2
swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}"); // x: 2, y: 1
}
}
둘 다 ref
적어줘야 한다. 또는 다음과 같이도 가능
class Program {
static void swap (Integer x, Integer y) {
int tmp = x.i;
x.i = y.i;
y.i = tmp;
}
public static void Main (static[] args) {
Integer x = new Integer(1);
Integer y = new Integer(2);
Console.WriteLine($"x: {x.i}, y: {y.i}"); // x: 1, y: 2
swap(x, y);
Console.WriteLine($"x: {x.i}, y: {y.i}"); // x: 2, y: 1
}
}
뭐 그렇다. 객체쓰니까.
다음과 같이 가변적으로 받을 수 있다.
void func (params int[] args) {
/* ... */
}
그렇다. 배열이기에 int[]
로 들어가는걸 주의.
메서드 구분 정보. 이걸 기준으로 override인지 overload인지 구분한다.
- 이름
- 매개변수 개수와 타입
반환 타입은 포함되지 않음. 주의하자.
static
수정자를 갖는 생성자를 정의할 수도 있다고 함. 클래스의 static fields를 초기화 할 때 사용되며, Main() 보다 먼저 실행됨.
new
로 부르는 것이 아니니, 매개변수와 접근 수정자를 가질 수 없다고 한다.
다시말하자면, static fields를 init 하는 방법에는 다음 두 개가 있다는 것.
- 선언과 동시에 초기화
- static constructor를 통해 초기화
소멸 시 호출됨.
class Program {
~Program () { /* ... */ }
}
이렇게 하면 compile 시 Finalize()
메서드가 된다고 한다. 참고로 얘는 override 불가능.
scope 벗어나면 즉시 호출됨
property의 매개변수 받기? 뭐 이렇게 볼 수가 있을까?
class Data {
private int[] data = new int[100];
public Data this[int i] {
get { return data[i]; }
set { data[i] = value; }
}
}
/* ... */
Data d = new Data();
d[0] = 10;
Console.WriteLine(d[0]); // 10
뭐 이렇게 사용하는 애다. 참고로 indexer의 modifier는 static
을 사용할 수 없음. this
컨텍스트를 사용하니까 그런듯.
재정의는 위에서 봤을 것이고, type conversion도 overload가 가능하다. 이걸 user-defined type conv. 라고 함.
public static explicit operator <type-name> ( ... ) { ... }
- 명시적
public static implicit operator <type-name> ( ... ) { ... }
- 묵시적
수정자가 개많이붙는다는걸 알아두자. public, static, explicit/implicit, operator. 총 네 개가 붙는다.
델리게이션 자체는 위에서 다뤘고, 델리게이션에 여러 메서드를 연결할 수도 있다고 함. 이렇게 연결한 경우 한 번에 연결된 애들을 호출한다고 한다. 약간 함수형? 이런느낌
+
연산자로 연결-
연산자로 제거
연결 순서로 호출된다.
물론 연결되는 애든 모두 동일한 return type, argument length/type을 가져야겠지.
delegate를 통해 event를 처리한다고 한다. 성격이 비슷하긴 한데...
// 델리게이트
delegate void Dele (int a);
class Program {
// 이벤트 델리게이트
public event Dele evt; // 이벤트의 타입은 델리게이트임
// 등록할 메서드
public void check (int n) {
if (n % 2 == 0) { // 만약 조건에 부합하면
evt(n); // 파라메터와 함께 이벤트 호출
}
}
// 이벤트 처리기
static void isEven (int i) {
Console.WriteLine($"{i}는 짝수임");
}
static void Main (string[] args) {
Program p = new Program();
p.evt += new Dele(isEven); // add event
p.check(1) // nothing
p.check(2) // 2는 짝수임 (emit event)
}
}
이해되나? 이벤트 추가/제거는 델리게이트와 똑같다.
evt += new Dele(method);
: 추가evt -= new Dele(method);
: 제거
이걸 System.EventHandler
델리게이트 사용해 윈폼 이벤트 핸들링이 가능하다.
클래스와 동일하게 객체의 구조와 행위를 명시(정의)하는데, 차이점은 다음과 같다.
- class: ref.
- heap에 저장됨
- param같은걸로 넘길 때, ref가 넘어감
- struct: value
- stack에 저장됨
- param같은걸로 넘길 때, value(내용)가 넘어감
- 상속불가
- destructor 구현불가
- member는 초기값 못가짐
다음과 같은 형태를 가지며,
public struct StructName {
// member
}
수정자는 다음과 같다.
- public
- protected
- internal
- private
- new
이 차이점을 알아두자.
상속은 재사용성 증가시키지. c#은 클래스 하나만 상속할 수 있는 단일 상속 만을 지원한다.
class Parent {
private int data = 0; // 만약 상속받는 클래스에서 'data' 필드가 없다면 얘가 상속됨
public Parent (int a) { /* ... */ }
public void print () { /* ... */ }
}
class Child: Parent {
private int data = 0; // 상속받는 클래스에서 부모 클래스의 필드인 'data'가 다시 정의되었으니, 부모의 'data'는 숨겨짐
public Child (int a, int b): base (a) { // 부모 클래스의 생성자로 변수 a를 넘김
/* ... */
}
public void print () {
base.print(); // 부모 클래스의 print() 메서드 호출
/* ... */
}
}
구현은 했는데, 파생 클래스에서 재정의가 필요. 여기서 어떤 지정어를 사용해 재정의했는지에 따라 동작이 달라진다.
new
지정어로 재정의: 선언 type에 따라 호출됨override
지정어로 재정의: 구현 type에 따라 호출됨
public class Program {
public static void Main() {
A ab = new B();
A ac = new C();
C cc = new C();
ab.print(); // B
ac.print(); // A
cc.print(); // C
}
}
abstract class A {
public virtual void print () {
Console.WriteLine("A");
}
}
class B: A {
public override void print () {
Console.WriteLine("B");
}
}
class C: A {
public new void print () {
Console.WriteLine("C");
}
}
재정의 시 modifier는 항상 일치 해야만 한다.
virtual method가 포함된 클래스는 반드시 abstract
키워드를 붙여야만 한다.
얘는 virtual과 비슷하긴 한데, 몸체가 없는애.
재정의 못하는애. 이걸 한 번에 모든 메서드에 대해 적용하는 것이 sealed class.
자식 클래스가 부모 클래스로 conversion은 가능한데, 부모 클래스가 자식 클래스로 conversion은 불가능하다. 이유는 자식 클래스는 부모 클래스에서 추가적으로 기능들이 구현된 것이기 때문. 즉, 해당 기능이 구현되어있지 않으니 Exception이 발생되는 것.
interface
키워드 사용하는 애. 멤버로는 다음이 올 수 있다.
- method
- property
- indexer
- event
주의해야 할 것은, 모두 구현부가 없다는 것과 모두 public 이라는 것. 얘네들 중 하나라도 구현하지 않으면 해당 클래스는 abstract class이다.
때문에 다중 상속이 가능하다. 여러개의 interface를 받을 수 있다는 것. 물론 클래스 extends와 함께 인터페이스의 implement도 가능하다.
class Program: BaseClass, Interface_A {
/* ... */
}
class A: Interface_A { /* ... */ }
class B: A, Interface_B { /* ... */ }
수정자는 다음과 같다.
- public
- protected
- internal
- private
- new
위에서 설명했지. 참고로 여러개 받을 수 있다.
// generic class
class Program<T1, T2, T3> { /* ... */ }
// generic interface
interface IF<T1, T2> { /* ... */ }
class Impl<T1, T2>: IF<T1, T2> { /* ... */ }
// generic method
class M {
static void swap <T> (ref T a, ref T b) {
T tmp = a;
a = b;
b = tmp;
}
public static void Main (string[] args) {
int a = 1, b = 2;
Console.WriteLine($"a: {a}, b: {b}"); // a: 1, b: 2
swap<int>(ref a, ref b);
Console.WriteLine($"a: {a}, b: {b}"); // a: 2, b: 1
}
}
JAVA와는 달리 generic이 꼭 class일 필요는 없다. alias라서 그런듯?
제네릭의 범위를 어떤 서브클래스로 제한할 수 있다.
class Parent { /* ... */ }
class Child: Parent { /* ... */ }
class Program<T> where T: Parent { /* ... */ }
/* ... */
Program<Child> p1 = new Program<Child>(); // OK
Program<int> p2 = new Program<int>(); // ERROR
Parent
부분에는 클래스 말고도 다양한 애들이 들어갈 수 있다.
where T: struct
:T
는 valuewhere T: class
:T
는 ref(참조형)where T: new()
:T
는 매개변수가 없는 생성자가 있어야 함where T: MyClass
:T
는MyClass
클래스의 파생 클래스- 물론 여기서의 '파생 클래스'는 abstract class가 아니겠지
where T: MyInterface
:T
는MyInterface
인터페이스를 구현한 클래스
실제 동작 코드는 아니고, JAVA의 annotation(@)과 같은애. 개발자들끼리 소통?그런거 하려고 쓰는애임.
정확히는 해당 클래스, 필드, 메서드, 프로퍼티 뭐 이런애들의 속성에 대한 정보를 주고받기 위해 사용하는 애다. assemble에 metadata 형식으로 추가된다는데... 뭐 이건 그렇다고 치자.
attribute에는 두 가지 종류가 있다.
- 표준 attribute (.NET FW에서 제공)
- 사용자 정의 attribute
조건적으로 실행하고 안하고 할 Conditional attribute 와, deprecated 예정인 애를 나타낼 Obsolete attribute 두 개를 소개하고 있다.
참고로 Conditional attribute는 System.Diagnostics
를 사용해야 한다니 주의하도록 하자.
class ObsoloAttr {
[Obsolete("경고, Deprecated 예정인 애")]
public static void LegacyMethod () { /* ... */ }
/* ... */
}
LegacyMethod
사용하는 코드를 컴파일하면 뭐라뭐라 저 메시지와 함께 warning이 뜬다.
System.Attribute
클래스에서 파생시켜 정의가 가능. 이름 형태는 XxxAttribute
이렇게 하라는데... 강제일까? 그건 잘 모르겠다.
정의나 뭐 그런건 똑같은 클래스이며 attribute라서 큰 차이는 없다.
class TestAttribute: Attribute { /* ... */ }
[TestAttribute( /* ... */ )]
runtime 때 발생되는 에러. 이런 애를 처리해줘야(exception handling) 안전한 프로그램이겠지. 물론 이런 방법들은 언어 자체에서 제공해준다.
예외도 하나의 객체다. 따라서, 예외를 위한 클래스를 정의해줘야 할 것. 그냥 일반적인 클래스로 취급해주면 된다.
class MyException: ApplicationException {
public MyException (string s): base(s) { }
}
class Program {
public static void Main (string[] args) {
try {
throw new MyException("My exception");
} catch (MyException e) {
Console.WriteLine(e.Message); // My exception
}
}
}
constructor로 string 넘겨주면 된다. 뭐 이렇게 해 주면 됨.
참고로 우리가 정의해주고 안해주고에 따라 컴파일러의 동작이 달라지는데, 일반적인 시스템 예외의 경우 컴파일러가 try-catch
로 잡아주지 않아도 신경안쓰지만, 우리가 정의해준 예외의 경우 try-catch
로 잡아주지 않으면 컴파일러가 컴파일하지 않는다고 한다.
또한 시스템에 발생된 의도치않은 예외를 implicit exception 이라고 하고,
throw
를 통해 의도적으로 발생시킨 예외를 explicit exception 이라고 한다.
다음이 있다.
ArithmeticException
: 산술 연산IndexOutOfRangeException
: 이름 그대로 인덱스 벗어날 때 발생됨ArrayTypeMismatchException
: 타입에러InvalidCastException
: casting(explicit type conversion) 실패 시NullReferenceException
: null 객체 참조 시OutOfMemoryException
: 메모리 할당 실패 시
다음과 같이 try-catch-finally
구문을 사용ㅎ나다.
try {
/* ... */
} catch (ExceptionType exceptionName) {
/* ... */
} catch (ExceptionType exceptionName) {
/* ... */
} finally {
/* ... */
}
알고있듯이, finally
블록은 반드시 실행된다. goto문 이런걸로도 빠져나올 수 없음.
예외 처리가 해당 메서드에 없으면, 해당 메서드를 호출한 메서드로 예외를 전파함. 예외 이후 코드는 모두 무시된다.
이렇게 try-catch
문을 찾을 때까지 전파한다. 스택타고 올라간다 생각하면 됨.
프로세스 단위로 자원공유하며 여러가지 행동을 한 번에 진행할 수 있다는 것은 OS에서 배웠지. c#도 물론 스레드를 지원한다.
c#에서의 thread는 method 단위로 실행하며, 이를 위해 Thread
클래스와 System.Threading.ThreadStart
델리게이트를 지원한다.
class Program {
static void ThreadBody () {
Console.WriteLine("Thread body");
}
public static void Main () {
ThreadStart ts = new ThreadStart();
}
}