- c#의 기본 자료형은 double
- 속성 기본형
public int a { get; set; }
- 속성 접근제한자 변경(생성자에 의한 값 변경의 구조를 만들 때 씀)
public int a { get; private set; }
- 자동완성 프리셋을 프로그래밍적 용어로 코드 스니핏(Code Snippet)이라고 한다. 보통 탭 두번 생성자 스니핏(ctor입력 후 + 탭 두번)
- c# .NET 기본 구조체 System.DateTime, System.TimeSpan, System.Drawing.Color
- c#의 구조체에는 생성자와 메서드(함수)가 존재한다!! 그리고 똑같이 new로 생성함
class MyClass {
public int X { get; set; }
public int Y { get; set; }
}
struct MyStruct {
public int X { get; set; }
public int Y { get; set; }
}
MyClass myClass = new MyClass { X=1, Y=2 };
MyStruct myStruct = new MyStruct { X=1, Y=2 };
객체를 메모리 상에 저장하는 방식의 차이
클래스는 변수가 있는 곳과 다른 곳에 객체의 영역이 확보되고 변수에는 그 참조가 저장되며 (힙 영역에 할당) 상속할 수 있다.
구조체는 변수 자체에 객체가 저장되며 상속이 없다. (스택 영역에 할당)
기본적으로 val&ref는 프로그램이 실행되는 효율과 메모리 공간의 효율적 활용의 이유 때문에 존재한다.
c#의 모든 자료형은 값형과 참조형 중 하나로 반드시 분류된다.
값형과 참조형이 동작하는 차이는 대입할 때뿐만 아니라 객체를 메서드의 인수에 넘겨줄 때도 마찬가지!
클래스는 참조형 객체를 인수로 지정하면 객체를 가리키는 참조가 복사되어 메서드에 전달된다.
따라서 이렇게 넘겨받은 객체의 값을 메서드 안에서 변경하면 호출한 쪽의 객체가 그 참조를 통해 변경된다.
Value Type - 구조체, int, long, decimal, char, byte, 모든 열거형
Reference Type - 클래스, object, string
참조형에서 초기값인 null 키워드는 아무것도 참조하지 않는 상태이고 상수이다.
if(item==null)
과 같은 식으로 판별가능 하며 string 문자열에서도 null은 ""과 같은 의미
값형에서는 일반적으로 null을 지정할 수 없고, 지정하게 하려면 nullable(Nullable< T >형)을 이용해야한다.
nullable은 형이름에 ?를 붙여서 표현 ex) int? num = GetNumber();
int? num = null; // 형 이름에 ?를 붙이면 nullable 형이 된다.
if(num.HasValue) ~ // HasValue속성을 통해 null 이외의 값이 지정됐는지 조사할 수 있다.
두 가지의 차이점
기본적으로 Call by Value/ Reference의 차이처럼 값의 상관관계적인 면의 차이.
VType a = new VType();
VType b = a; // a 수정해도 b는 노 영향
RType c = new RType();
RType d = c; // c 수정하면 d도 수정 됨
static 키워드 붙은 것들. new 생성할 필요가 없다.
정적 클래스에는 인스턴스 속성이나 인스턴스 메서드가 없고 인스턴스를 생성할 수 없다.
struct void Main(string[] args) {
//Today는 static 속성
DateTime today = DateType.Today;
//Console은 static 클래스, WriteLine은 static 메서드
Console.WriteLine("오늘은 {0}월 {1}일 입니다.", today.Month, today.Day);
}
용도와 목적
만일 Product.Price라고 써서 상품의 가격을 얻을 수 있다면 그것은 어떤 상품의 가격인지 모를수 있다.
여러 인스턴스별로 달라야하지 않는 값 그리고 그 상위 클래스/구조체 자체에 연관된 값들은 스태틱으로 정적으로 생성해 인스턴스를 생성하지 않고 이용할 수 있다.
using으로 네임스페이스를 지정함으로써 형 이름만으로 클래스를 사용할 수 있는 기능이 있다.
class Program {
static void Main(string[] args) {
System.Console.WriteLine("Hello! C# world.");
}
}
using System;
class Program {
static void Main(string[] args) {
Console.WriteLine("Hello! C# world.");
}
}
같은 것.
여러 클래스가 하나의 네임스페이스에 소속돼 있을 경우에는 using 지시자로 그 네임스페이스를 입력할 필요가없다.
is a 관계가 성립되어야한다. 'A는 B이다.'가 성립해야한다는 뜻이다. (완전한 포함관계)
하지만, 'A는 B로 만들어져 있다.', 'A는 B를 가지고 있다.'와 같은 관계라면 상속을 사용할 수 없다!!
모든 클래스는 Object를 상속하고 있으므로
object person = new Person();
object employee = new Employee();
이렇게 하면 여러 가지 형의 객체를 저장하는 배열을 정의하거나 여러 가지 형의 객체를 받는 메서드를 정의할 수 있다.
컴파일 되어 실행할 수 있는 코드를 어셈블리라고 하는데 using에서 참조가 안될 경우
'참조 추가'를 통해 해당 어셈블리(dll이나 exe) 파일을 프로젝트에 추가 시키면 된다.
C#으로 제작된 응용 프로그램이 시작될 때 처음에 호출되는 Main 메서드는 정적 메서드로 정의해야 한다는 C# 프로그래밍 규약이 있다.
따라서 Main 메서드에서 직접 호출되는 메서드가 있다면 그 메서드도 정적 메서드로 정의해야한다.
인스턴스 메서드를 호출하려면 어느 인스턴스인지를 지정해야 하는데
Main 메서드는 인스턴스가 존재하지 않는 상태로 동작하므로 해당 인스턴스를 특정할 수 없다.
메서드는 그 기능을 단순하게 만들어야 한다.
함수를 여러개 만들어도 좋으니 Main 메서드가 더럽다면 나눠서 메서드의 기능을 최대한 단순화 시키는 방향으로 만드는게 좋은 코드다.
클래스로 분리하면 코드가 더욱 더 단순화되고 전체적인 코드의 양도 줄어든다. 메서드의 기능 분리와 같은 개념.
인스턴스 생성의 코드가 있다면 for문 안에 쓰는 것은 바람직하지 않다.
인스턴스 속성이나 인스턴스 필드를 이용하지 않는 메서드는 정적 메서드로 만들 수 있고 만들자!
클래스 안에 모든 멤버가 정적 멤버일 경우에는 클래스를 정적 클래스로 지정할 수 있고 그렇게 만들자!
c# 완전초기에는 클래스에 static을 못 붙여서 생성자를 private으로 지정해서 인스턴스를 못 생성하게 했지만 지금은 그럴 필요가 없다.
고정된 값을 상수로 변경하는 작업 기기
const 키워드 - public으로 지정하지 않는 것이 좋다. private일 경우는 상관없지만, public 참조가 가능하다면 버전 관리에 관한 문제가 발생 할 수 있다.
따라서 public으로 지정해서 다른 클래스가 접근할 수 있게 한 경우는 const대신 static readonly를 사용하는 것이 좋다.
빌드할 때 참조하는 const, 실행할 때 참조하는 readonly 를 생각했을 때 값 수정시 안정적이게 적용되니까
반환값이 있는 메서드에는 무엇이 반환되는지를 유추할 수 있는 이름을 붙이는 것이 바람직하다.
ex) 매출 데이터를 읽어들여서 Sale 객체 리스트를 반환하는 함수일 경우 ReadSales(~) 라는 이름을 짓는게 좋음.
초보 프로그래머는 이 경우에 LoadFile 이나 ReadCsv라고 이름을 짓기도 하는데 어떤 데이터가 반환되는지 정확히 알기 어렵다.
변수 이름을 붙일 때는 단수형과 복수형을 구별하는 것이 좋다.
읽어 들인 행을 한 행씩 처리하는 데는 foreach 문을 사용한다.
개인적으로 idx를 사용하는지 여부를 생각하는게 좋고, 조건별로 처리할 일이 있으면 무조건 for문이다.
그런 일없으면 foreach가 실수를 줄일 수 있다.
Sale sale = new Sale {
ShopName = items[0],
ProductCategory = items[1],
Amount = int.Parse(items[2])
};
Sale sale = new Sale();
sale.ShopName = items[0];
sale.ProductCategory = items[1];
sale.Amount = int.Parse(items[2]);
같은 의미고 장점은 유지보수하는 과정에서 행과 행 사이에 다른 코드가 들어갈 수도 있는 것을 방지할 수 있다.
클래스 멤버변수에 붙여서 함수 지역변수와 차이를 만들어 낼 수 있다.
dict.ContainsKey(key) 로 키 값에 해당하는 Value 값을 구할 수 있다.
foreach 돌려서 모든 값을 참조할 때 KeyValuePair a로 a.Key, a.Value에 접근가능하다.
Dictionary<string, int> amountPerStore = new Dictionary<string, int>();
amountPerStore = sales.GetPerStoreSales();
이런식으로 작성해선 안 된다. ref타입이여서 new 로 생성된 객체가 생성된 채로 여전히 남아있게 되버리니까
I인터페이스를 자료형으로 선언한 변수/자료구조에 그 인터페이스를 사용한 자료구조를 담아 처리 가능.
List<int> list = new List<int>() { 1, 2, 3, 4, 5 };
ICollection<T> collection = list;
var count = collection.Count;
인터페이스 = 규격이라고 생각하는게 좋다.
3가지 기억
- A클래스가 I인터페이스를 구현했다면 A객체는 I형 변수에 대입될 수 있다.
- I형 변수는 I인터페이스가 정의하는 속성과 메서드를 사용할 수 있다.
- 속성이나 메서드가 실행할 수 있는 구체적인 동작은 I인터페이스가 아니라 A클래스에 작성된다.
메서드의 반환값이나 인수를 인터페이스로 지정하면 어떤 점이 좋아지나?
- 프로그램을 수정하기 좋아진다.(리스트나 배열을 둘 다 받아올 수 있다.)
- 생성자 안에서 객체가 오버라이드 되지 않는다. (IEnumerable에는 List 클래스에 정의돼 있는 Add나 Remove같은 그 상위 클래스에 있는 함수가 없으니까)
- 다른 자료구조로 변경하게 될 때, 같은 인터페이스를 구현하는 자료구조라면 호출하는 클래스는 수정하지 않아도 된다.
List<int> list = new List<int>();
ICollection<int> collection = list;
IEnumerable<int> enumerable = list;
new를 사용해서 생성할 때 =의 좌변과 우변이 같은 경우 var을 사용해서 조금 더 코드를 단순화 시키는 방법을 사용하자.
foreach에서도 var 사용!!
주의할 점 dynamic 대신 var 사용 금지!!
대입할 때 우변에 있는 변수의 형이 분명하지 않다면 사용 ㄴㄴ
var을 사용했다면 변수이름에 자료형을 표현하지 않도록 한다.(무슨 값이 들어갈지 사실 모르는 거니까 혼란을 초래)
C# 3.0 부터 도입된 람다식은 언어로서의 표현력을 크게 진화시켰다.
메서드를 인수로 넘겨주는 방법을 delegate로 했었다면 더 편한 람다식이 있다.
다른 배열로 몇개의 동일한 숫자가 있는지 식별하는 함수가 있다고 하면
public int Count(int [] numbers, int num) {
int count = 0;
foreach(var n in numbers) {
if(n==num) count++;
}
return count;
}
다른 조건으로 카운트하려고 할 경우에는 이 메서드를 사용할 수 없다.
만약에 Count 함수에 있는 if문 자체를 인수로 받아들일 수 있다면 더욱 편리할 것이다.
public int Count(int [] numbers, Method judge) {
int count = 0;
foreach(var n in numbers) {
if(judge(n)==true) count++;
}
return count;
}
이런 식으로 말이다.
c# 1.0에선 이 방법으로 델리게이트를 사용했다.
public delegate bool Judgement(int value);
public int Count(int [] numbers, Judgement judge) {
int count = 0;
foreach(var n in numbers) {
if(judge(n)==true) count++;
}
return count;
}
~~
public void Do() {
var numbers = new[] { 5, 3, 9, 6, 7 };
Judgement judge = IsEven;
var count = Count(numbers, judge);
Console.WriteLine(count);
}
//or 위 코드나 아래코드로 가능 delegate
public void Do() {
var numbers = new[] { 5, 3, 9, 6, 7 };
var count = Count(numbers, IsEven);
Console.WriteLine(count);
}
public bool IsEven(int n) {
return n%2==0;
}
조금 더 편리한 방법 C# 2.0에선 익명 메서드를 이용했다.
Predicate는 .Net 2.0에서 부터 이용가능한 델리게이트의 제너릭 버전이다.
Predicate 델리케이트는 어떤 기준을 만족하는지를 판단하는 메서드를 나타낸다.
보통 배열이나 리스트같이 여러 개의 요소를 다루는 객체에 소속된 메서드의 인수로 사용된다.
public int Count(int [] numbers, Predicate<int> judge) {
int count = 0;
foreach(var n in numbers) {
if(judge(n)==true) count++;
}
return count;
}
public void Do() {
var numbers = new[] { 5, 3, 9, 6, 7 };
var count = Count(numbers, delegate(int n) { return n%2==0; } );
Console.WriteLine(count);
}
델리게이트에 익숙하지 않은 초보 프로그래머에게는 장애물이였다.
그래서 C# 3.0에선 람다식을 도입했다.
var count = Count(numbers, n => n%2 == 0 );
처음에 이해하기 어렵다.
Predicate<int> judge =
(int n) => { // 여기부터 람다식 judge 변수에 대입한다.
if (n%2 == 0)
return true;
else
return false;
};
var count = Count(numbers, judge);
람다식은 일종의 "메서드" delegate 키워드가 없어지고 그 대신 => (람다 연산자)가 사용된다.
(인수 선언) => { (메서드 본문) }
대입하는 즉시, 본문이 실행되지 않는다.
judge 변수는 대입하고 나서 바로 Count의 인수로 전달되므로 이 judge 변수를 없애고 식을 직접 Count 메서드의 인수로 지정하는 코드
var count = Count(numbers,
(int n) => {
if (n%2 == 0)
return true;
else
return false;
}
);
return의 오른쪽에 식을 쓸 수 있고 bool 값을 갖으므로 if문을 없앨 수 있다.
var count = Count(numbers, (int n) => { return n%2 == 0; } );
람다식에서 {}가 1개의 명령문을 포함할 때는 {}와 return을 생략할 수 있다.
var count = Count(numbers, (int n) => n%2 == 0 );
람다식에서는 인수의 형을 생략할 수 있다. 컴파일러가 형을 제대로 추론해주므로 형을 명시하지 않아도 됨.
var count = Count(numbers, (n) => n%2 == 0 );
인수가 1개인 경우에는 ()를 생략할 수 있다.
var count = Count(numbers, n => n%2 == 0 );
람다식의 예) 숫자에 1이 포함된 수의 개수를 센다.
var count = Count(numbers, n => n.ToString().Contains('1') );
람다식의 장점은
추상도가 높아진다. == '어떻게 하는가(How)'가 아니라 '무엇을 하는가(What)'을 생각하며 코드를 작성할 수 있다.
.Net 프레임워크에 프리셋 메서드가 존재한다.
리스트 클래스에는 델리게이트(람다식)을 인수로 받는 메서드가 많이 있다.
아래와 같은 리스트 객체가 있다고 가정해보자.
var list = new List<string> {
"Seoul", "New Delhi", "Bangkok", "London", "Paris", "Berlin", "Canberra", "Hong Kong"
};
인수로 지정한 조건에 일치하는 요소가 존재하는지를 조사하고 true나 false를 반환한다.
var exists = list.Exists(s => s[0] == 'A');
Console.WriteLine(exists);
결과 false(0) 출력
인수로 지정한 조건과 일치하는 요소를 검색하고 처음 발견된 요소를 반환한다.
var name = list.Find(s => s.Length == 6);
Console.WriteLine(name);
결과 London 출력
인수로 지정한 조건과 일치하는 요소를 검색하고 처음 발견된 요소의 인덱스를 반환한다.
var index = list.FindIndex(s => s == "Berlin");
Console.WriteLine(index);
결과 5 출력
인수로 지정한 조건과 일치하는 모든 요소를 찾는다. 이 메서드의 반환값은 List< T > 형 이다.
var names = list.FindAll(s => s.Length <= 5);
foreach (var s in names)
Console.WriteLine(s);
결과 Seoul과 Paris 출력
배열에서 FindAll 쓰는 법
Array.FindAll(arrayOfName, s => s.Length <= 5);
인수로 지정한 조건과 일치하는 모든 요소를 리스트에서 삭제한다. 반환값은 삭제한 요소의 개수
var removeCount = list.RemoveAll(s => s.Contains("on"));
Console.WriteLine(removeCount);
결과 London과 HongKong이 삭제되므로 2 출력
인수로 지정한 처리 내용을 리스트의 각 요소의 대상으로 실행한다.
이제까지 살펴본 예는 Predicate< T > 델리게이트를 인수로 받는 메서드였지만
이 ForEach 메서드는 반환값이 void이고 한 개의 인수를 받는 Action< T > 델리게이트를 인수로 받아들인다.
list.ForEach(s => Console.WriteLine(s));
아래와 같은 코드
foreach(var s in list)
Console.WriteLine(s);
다른 더 간단한 표현
list.ForEach(Console.WriteLine);
결과 모든 도시 출력
리스트 안에 있는 요소를 다른 형으로 변환하고 변환된 요소가 저장된 리스트를 반환한다.
var lowerList = list.ConvertAll(s => s.ToLower());
lowerList.ForEach(s => Console.WriteLine(s));
변환한 것을 lowerList에 대입. list 자체는 변화하지 않는다.
결과 모든 도시 Lower(소문자로) 출력
LINQ란 Language Integrated Query를 축약한 것 C# 3.0 부터 도입된 기능.
LINQ를 사용하면 객체, 데이터, XML과 같은 다양한 데이터를 표준화된 방법으로 처리할 수 있다.
메서드 체인(.으로 이어서 처리할 수 있는게)이 중요한 특징이기도 하다.
LINQ to Object는 여러 객체를 입력 데이터로 취급한다.
using System;
using System.Collections.Generic;
using System.Linq;
~
var names = new List<string> {
"Seoul", "New Delhi", "Bangkok", "London", "Paris", "Berlin", "Canberra", "Hong Kong"
};
IEnumerable<string> query = names.Where(s => s.Length <= 5);
foreach(string s in query)
Console.WriteLine(s);
using Linq; 로 LINQ를 사용가능.
표준 쿼리 연산자(Where, Select ...)가 조작하는 데이터를 시퀀스라고 한다.
시퀀스 중에서 가장 흔한 것이 배열과 리스트
IEnumerable< T > 인터페이스를 구현한 객체는 모두 시퀀스로 간주된다.
Where 메서드는 시퀀스에서 조건을 만족하는 것만 추출하는 메서드
FindAll 메서드와 다른점은 IEnumerable 인터페이스를 구현하고 있는 형이기만 하면 Where 메서드를 이용할 수 있다.
또한 반환/매개변수 형이 동일해서 메서드 체인을 이용할 수 있다.
IEnumerable<string> query = names.Where(s => s.Length <= 5)
.Select(s => s.ToLower());
foreach(string s in query)
Console.WriteLine(s);
Select 메서드는 각 요소에 대해 람다식을 지정한 변환 처리를 수행한다.
실제 코드에서는 var를 사용해서 더 간단하게 표현한다.
var query = names.Where(s => s.Length <= 5)
.Select(s => s.ToLower());
Where과 Select같은 것을 쿼리 연산자라고 한다.
쿼리 연산자 | 실행 형태 | 설명 |
---|---|---|
Where | 지연 실행 | 조건에 따라 값의 시퀀스를 필터링 처리한다. |
Skip | 지연 실행 | 시퀀스 안에 지정된 개수만큼의 요소를 건너뛰고 남은 요소를 반환한다. |
Count | 즉시 실행 | 시퀀스에 있는 요소의 개수를 반환한다. |
ToList | 즉시 실행 | 시퀀스로 List< T > 를 생성한다. |
실제로 데이터가 필요할 때 쿼리가 실행된다.
string[] names = {
"Seoul", "New Delhi", "Bangkok", "London", "Paris", "Berlin", "Canberra", "Hong Kong"
};
var query = names.Where(s => s.Length <= 5); // query 변수에 대입한다.
foreach(var item in query)
Console.WriteLine(item);
Console.WriteLine("------");
name[0] = "Busan"; // names[0]을 수정한다.
foreach(var item in query) // query의 내용을 다시 꺼낸다.
Console.WriteLine(item);
실행결과
Seoul
Paris
------
Busan
Paris
호출됐을 때 쿼리가 실행되며 그 결과가 배열에 저장된다.
ToArray 메서드를 사용해 즉시 실행
string[] names = {
"Seoul", "New Delhi", "Bangkok", "London", "Paris", "Berlin", "Canberra", "Hong Kong"
};
var query = names.Where(s => s.Length <= 5 ).ToArray(); // 여기서 배열로 변환한다.
foreach(var item in query)
Console.WriteLine(item);
Console.WriteLine("------");
names[0] = "Busan";
foreach(var item in query)
Console.WriteLine(item);
실행결과
Seoul
Paris
------
Seoul
Paris
LINQ에서는 메서드 호출 외에도 '쿼리 구문'이라는 SQL과 비슷한 구문도 사용할 수 있다.
var query = from s in names
where s.Length >= 5
select s.ToUpper();
foreach (string s in query)
Console.WriteLine(s);
아래는 메서드 구문
var query = names.Where(s => s.Length >= 5)
.Select(s => s.ToUpper());
foreach (string s in query)
Console.WriteLine(s);
쿼리구문을 잘 안쓰는 이유는 3가지와 같다.
쿼리 구문은 LINQ의 모든 기능을 이용할 수 없다.
.으로 연결하는 메서드 구문이 생각을 방해받지 않고 연속해서 코드를 작성할 수 있다.
.으로 연결하는 메서드 구문은 인텔리센스 기능을 충분히 활용할 수 있다.
C# 3.0에서 도입된 기능이며 기존의 형에 새로운 메서드를 추가할 수 있다. 정의하려면 정적 클래스 안에서 첫 번째 인수에 this 키워드를 붙인 정적 메서드를 작성하면 된다.
namespace CSharpPhrase.Extensions {
public static class StringExtensions { // 확장 메서드를 정의하려면 정적 클래스로 지정한다.
public static string Reverse(this string str) { //정적 메서드로 지정하고 첫번째 인수에 this를 붙임. string에 Reverse메서드가 추가됨.
if(string.IsNullOrWhiteSpace(str))
return string.Empty;
char[] chars = str.ToCharArray();
Array.Reverse(chars);
return new String(chars);
}
}
}
네임스페이스를 using 지시자를 통해 지정하면 확장 메서드를 호출할 수 있다.
using CSharpPhrase.Extensions;
~~~
var word = "gateman";
var result = word.Reverse();
Console.WriteLine(result);
결과 : nametag
이디엄이라고 하기도 한다.
var age = 25;
변수를 초기화하는 나쁜 예
int age;
age = 25;
유지보수를 계속해감에 따라 변수 선언과 초기화 사이에 다른 코드가 끼어들 수도 있어서 코드의 가독성을 잃을 위험이 있다.
또한 초깃값이 무엇인지도 명확히 알 수 없게 되므로 비추천.
변수 선언과 초기화는 동시에 이뤄져야 하는 것이 원칙
컬렉션 초기화 구문을 사용한 관용구는 배열이나 리스트에 설정할 값이 이미 정해져 있을 경우에 이용한다. C# 3.0에서 도입된 기능
var langs = new string[] { "C#", "VB", "C++", };
var nums = new List<int>[] { 10, 20, 30, 40, };
마지막 요소 "C++", "40" 뒤에 , 콤마가 붙어있는 점에 주목해야한다. 마지막 요소에 붙인 콤마는 생략해도 되지만 가능하면 콤마를 붙이는 편, 요소를 바꾸거나 추가할 때 매우 편하기 때문이다.
var dict = new Dictionary<string, string>() {
{ "ko", "한국어" },
{ "en", "영어" },
{ "es", "스페인어" },
{ "de", "독일어" },
};
C# 6.0 이후에는 이렇게 작성할 수 있다.
var dict = new Dictionary<string, string>() {
["ko"] = "한국어",
["en"] = "영어",
["es"] = "스페인어",
["de"] = "독일어",
};
C# 3.0도입, 생성자에서 지정할 수 없는 속성값을 초기화할 수 있다. 속성 초기화 처리는 인스턴스가 생성된 후에 실행된다.
var person = new Person {
Name = "홍길동",
Birthday = new DateTime(1995, 11, 23),
PhoneNumber = "012-3456-7890",
};
이렇게 하면 Person 객체의 속성인 Name, Birthday, PhoneNumber 에 값이 들어간다.
어떤 변수의 값을 판정하는 단순한 비교에서 비교하려는 변수는 비교 연산자의 왼쪽에 두자.
if(age <= 10) { ... }
잘못된 코드
if(10 >= age) { ... }
'나이가 열 살 이하인가'라고는 말하지만 '열 살이 나이 이상인가'라고는 말하지 않는다. 코드도 인간의 사고방식에 맞춰 써야 한다.
지정한 범위에 해당하는 수치가 있는지 조사할 때는 수치를 수직선 상에 나열한다.
비교 대상이 되는 변수를 모두 왼쪽에 쓰는 방식보다는 num을 MinValue와 MaxValue 사이에 두는 것이 직관적으로 알아보기 쉽다.
if(MinValue <= num && num <= MaxValue) {
~
}
잘못된 코드
if(num >= MinValue && num <= MaxValue) {
~
}
return 문에는 메서드의 실행을 중단시키고 호출하는 쪽에 제어를 돌려주는 기능이 있다.
만족하지 않는 조건을 메서드 앞부분에서 return 문으로 제외해서 코드를 읽기 편하게 하는 방법을 다음 코드에서 볼 수 있다.
if(filePath == null)
return;
if(GetOption() == Option.Skip)
return;
if(targetType != originalType)
return;
// 체로 걸렀으니 이제 구현하려는 처리
잘못된 코드
if(filePath != null) {
if(GetOption() != Option.Skip) {
if(target == originalType) {
// 구현하려는 처리
}
}
}
'체로 걸러 남은 것만 처리하는' 코드는 조건에 대해 잊어버리면서 코드를 읽어나갈 수 있으므로 편리하다.
반면에 '안쪽으로 파고드는' 코드는 코드를 읽기 위해 세 개의 조건을 모두 기억하고 있어야 하기에 읽기 힘들어지며 복잡해진다.
int? num = GetNumber();
if(num.HasValue) {
~
}
잘못된 코드
int? num = GetNumber();
if(num.HasValue == true) {
~
}
if 문의 괄호 안에는 bool 값을 가지는 식을 쓸 수 있다. num.HasValue 자체가 식이므로 일부로 또 다른 식을 만들어 표기할 필요가 없다.
결과에 따라 true나 false를 반환하는 메서드일 경우
return a == b;
잘못된 코드
if (a == b)
return true;
else
return false;
if (a == b)
return true;
return false;
var result = a == b;
return result;
bool result = false;
if(a == b)
result = true;
return result;
식을 바로 리턴해버리면 거추장스럽지 않게 무슨 처리를 하는지 한눈에 알아볼 수 있는 장점이 있다.
반복할 횟수를 알고 있을 경우에는 for 문을 사용하고 반복할 횟수를 모를 경우에 while문을 사용한다.
배열이나 리스트 같은 컬렉션에서 요소를 모두 꺼내서 어떤 처리를 할 경우에는 foreach 문을 사용한다.
foreach (var item in collection) {
// 꺼낸 item에 대한 처리를 한다.
}
LINQ를 사용하면 foreach보다 코드를 더 간결하게 작성할 수 있을 때가 많다.
따라서 반복 처리를 작성할 때는 LINQ, foreach, for의 순서로 관용구를 적용하는 것이 좋다.
List< T > 클래스에 대해서만 해당되는 내용인데 ForEach 메서드를 사용하면 foreach문을 사용하지 않아도 된다.
var nums = num List<int> { 1, 2, 3, 4, 5, };
nums.ForEach(n => Console.WriteLine("[{0}] ",n));
ForEach 메서드의 인수로 길이가 긴 람다식을 쓸 수 있는데 ForEach 메서드를 한 줄에 쓸 수 있는 길이로 한정하는 것이 좋다.
루프 내부의 처리가 여러 행으로 구성된 코드라면 foreach문을 사용하는 것이 일반적이다. 왜냐하면 ForEach 메서드에서는 break, continue, yield, return을 사용할 수 없다.
리스트에 있는 요소를 그대로 메서드의 인수로 전달 할 수 있고 반환값이 없는 메서드일 경우에는 다음과 같이 메서드 이름만 지정해도 된다.
var nums = num List<int> { 1, 2, 3, 4, 5, };
nums.ForEach(Console.WriteLine);
do-while 구문 사용
루프 도중에 처리를 중단하고 싶다면 break문을 사용한다.
'메서드가 한 가지 기능만 가진다'라는 원칙이 지켜진다는 전제가 있고, 루프에서 빠져나오면서 호출한 메서드로 돌아가려고 할 경우에는 다음과 같이 루프 안에서 return 문을 사용하면 된다.
var numbers = new int[] { 123, 98, 4653, 1234, 54, 9854 };
foreach(var n in numbers) {
if(n > 1000)
return n;
}
return -1;
?와 :기호 뒤에 쓸 코드가 고정값이나 변수처럼 단순한 식일 경우,
조건을 판단해서 참이냐 거짓이냐에 따라 각각 다른 값을 변수에 대입하고 싶을 때 조건 연산자(삼항 연산자)를 이용한다.
var num = list.Contains(key) ? 1 : 0;
변수 초기화와 관련된 관용구와 같은 것 조건에 따라 다른 값을 메서드의 인수에 넘겨줄 때도 불필요한 지역 변수를 도입하지 않고 메서드에 값을 넘겨줄 수 있다.
DoSomething(list.Contains(key) ? 1 : 0);
null인지 아닌지를 판단해서 처리를 분기해야 할 때가 있을 경우, null이면 기본값을 사용해야 할 경우가 특히 많다.
var message = GetMessage(code);
if(message == null)
message = DefaultMessage();
이런 코드를 null 합체 연산자(??) 를 사용하면 간결하게 작성 가능하다.
var message = GetMessage(code) ?? DefaultMessage();
C# 6.0에 추가된 null 조건 연산자(?.) null인지 아닌지를 판단하는 코드 가운데 또 하나의 전형적인 방법.
if(sale == null)
return null;
else
return sale.Product;
null 조건 연산자를 쓴 예
return sale?.Product;
sale 변수가 null이 아닐 때는 Product 속성값을 반환하고 null일 때는 Product 속성에 접근하지 않고, null을 반환한다.
배열이 null일 경우에도 사용가능하다.
Customer first = (customers = null) ? null : customers[0];
배열일 경우 점(.)을 붙이지 않아도 된다. null 조건 연산자를 쓴 예
var first = customers?[0];
null 조건 연산자와 null 합체 연산자를 동시에 사용할 수도 있다.
var product = GetProduct(id);
var name = product?.Name ?? DefaultName;
조건연산자, null 합체 연산자, null 조건 연산자를 사용하는 이유 -> 간결하게 쓸 수 있으며, 연산자를 사용한 코드에는 다른 코드가 끼어들 여지가 없기 때문이다.
C# 6.0 부터 이용할 수 있게 된 속성 초기화
public int MinimumLength { get; set; } = 6;
이전방식 생성자
class PasswordPolicy {
public int MinimumLength { get; set; };
~~~
public PasswordPolicy() {
MinimumLength = 6;
}
}
메서드를 호출해서 속성을 초기화할 수도 있다.
public string DefaultUrl { get; set; } = GetDefaultUrl();
예전에 속성을 쓰던 방식
private string _name;
public string Name {
get { return _name; }
set { _name = value; }
}
예전에 속성 초기화하는 처리를 지연시키는 방법
private string _name;
public string Name {
get {
if (_name == null)
_name = GetNameFromFile();
return _name;
}
set { _name = value; }
}
값을 수정할 수 없는 읽기 전용 속성을 정의하고 싶을 때 기존 방법
get 접근자만 사용하면 클래스 안에서도 수정이 불가능하다.
pubilc class Person {
public string GivenName { get; private set; }
public string FamilyName { get; private set; }
public string Name {
get { return FamilyName + " " + GivenName; }
}
//생성자
public Person(string familyName, string givenName) {
FamilyName = familyName;
GivenName = givenName;
}
}
C# 6.0 새로운 방법
public class Person {
public string GivenName { get; private set; }
public string FamilyName { get; private set; }
public string Name => FamilyName + " " + GivenName;
}
get 접근자 본문을 하나의 식으로 표현할 수 있는 경우에는 => 연산자를 사용해 읽기 전용 속성을 정의할 수 있다.
읽기 전용 속성이 참조형인 경우(string은 제외)에는 참조는 수정할 수 없고, 속성이 가리키고 있는 객체는 수정할 수 있다.
예를 들어 List의 속성을 읽기 전용 속성으로 지정해도 List 컬렉션의 내용은 수정할 수 있다.
class Program {
static void Main(string[] args) {
var obj = new MySample();
obj.MyList.Add(6); // 읽기 전용이므로 여기서 빌드 오류가 발생한다.
obj.MyList.RemoveAt(0);
obj.MyList[0] = 10;
foreach(var n in obj.MyList) {
Console.WriteLine(n);
}
// obj.MyList = new List<int> (); 하지만 List<int> 자유롭게 이용할 수 있다.
Console.ReadLine();
}
}
class MySample {
public List<int> MyList { get; private set; }
public MySample() {
MyList = new List<int>() { 1, 2, 3, 4, 5 };
}
}
컬렉션 자체를 수정할 수 없게 하려면 다음과 같이 공개할 형을 IReadOnlyList< int > (.Net 4.5추가됨)나 IEnumerable< int > 로 지정해야 한다.
class MySample {
public IReadOnlyList<int> MyList { get; private set; }
public MySample() {
MyList = new List<int>() { 1, 2, 3, 4, 5 };
}
}
다음 코드처럼 여러 개의 인수를 받아들이지만 인수의 개수를 한정하고 싶지 않을 경우
var median = Median(1.0, 2.0, 3.0);
var median = Median(1.0, 2.0, 3.0, 4.0, 5.0);
메서드 오버로딩을 계속해서 구현할 필요가 없다.
가변 인수를 받아들이는 메서드를 정의할 때 params 키워드를 사용한다.
// 중간값을 구하는 메서드
private double Median(params double[] args) {
var sorted = args.OrderBy(n => n).ToArray();
int index = sorted.Length / 2;
if (sorted.Length % 2 == 0)
return (sorted[index] + sorted[index - 1]) / 2;
else
return sorted[index];
}
만약 Console.WriteLine이나 String.Format 처럼 인수를 지정해서 로그를 출력하는 메서드를 정의하고 싶을 때도 params 키워드를 사용한다.
private void WriteLog(string format, params object[] args) {
var s = String.Format(format, args);
WriteLine(s);
}
호출하는 예시
logger.WriteLog("Time:{0} User:{1} Message:{2}", time, user, message);
C# 4.0부터 도입된 옵션 인수를 사용하면 오버로드를 대체할 수 있다.
아래는 기존 3.0 오버로드의 예다.
public void DoSomething(int num, string message, int retryCount) { ...... }
public void DoSomething(int num, string message) {
DoSomething(num, message, 3);
}
public void DoSomething(int num) {
DoSomething(num, "DefaultMessage", 3);
}
인수의 개수가 적은 메서드에서 인수의 개수가 많은 메서드를 호출할 때 옵션 옵션 인수를 사용해서 해결할 수 있다.
private void DoSomething(int num, string message = "실패했습니다.", int retryCount = 3) {
~~~
}
변수에 1을 더할 때는 다음과 같이 쓰지 않고 ++연산자를 사용하는 것이 정석이다.
잘못된 코드
count+=1;
바른 코드
count++;
or
++count;
파일 경로를 지정하려면 문자열 앞에 @을 붙인 축자 문자열 리터럴을 이용하자.
var path = @"C:\Example\Greeting.txt";
축자 문자열 리터럴을 사용하면 \ 기호가 이스케이프 시퀀스로 인식되지 않으므로 파일 경로를 그대로 기술할 수 있다.
축자 문자열 리터럴에서 "(따옴표)를 표현해야 할 경우에는 ""와 같이 연속해서 써야한다.
잘못된 코드
var path = "C:\\Example\\Greeting.txt";
대표적인 관용구
var temp = a;
a = b;
b = temp;
c# 7.0이라면 튜플을 이용해 이렇게 사용할 수 있고 가능하면 이 방법 추천.
(b a) = (a b);
잘못된 코드
var temp1 = a;
var temp2 = b;
a = temp2;
b = temp1;
문자열을 숫자값으로 변환하려고 할 때 오류가 있는지 조사하기 위해 TryParse 메서드를 사용한다.
TryParse 메서드는 성공 시 true, 실패 시 false를 반환한다.
TryParse 사용하기 직전에 변수를 선언해야한다. 이 때 초기값은 설정하지 않아도 된다. (단, c# 7.0에선 int.TryParse(str, out var height)
처럼 쓰면 됨.)
int height;
if (int.TryParse(str, out height)) {
//성공 시 처리
} else {
//실패 시 처리
}
Parse 메서드를 사용해서 예외를 포착하는 코드를 작성하면 안 된다.
번잡해지고, 리소스에 부담이 되어 처리 속도도 늦어진다.
잘못된 코드
try {
int retryCount = int.Parse(str);
} catch (ArgumentNullException ex) {
~~~
} catch (FormatException ex) {
~~~
}
단, 문자열이 숫자로만 구성돼 있다는 보장이 있다면 다음 코드를 이용해도 괜찮다.
int height = int.Parse(str);
참조형 객체를 다른 형으로 형변환하려면 as 연산자를 사용한다.
var product = Session["MyProduct"] as Product;
if(product == null) {
// product를 가져올 수 없을 때 수행할 처리
} else {
// product를 가져왔을 때 수행할 처리
}
as 연산자는 값형에는 사용하지 않는다.
잘못된 코드
try {
var product = (Product) Session["MyProduct"];
// product를 가져왔을 때 수행할 처리
} catch (InvalidCastException e) {
// product를 가져올 수 없을 때 수행할 처리
}
일반적으로 발행하는 상황에 대해 예외를 사용하는 것은 바람직하지 않다.
예외를 포착한 후에 다시 예외를 던지려면 다음과 같이 throw만 기술해야한다.
try {
~
} catch (FileNotFoundException ex) {
// 예외 정보를 사용한 어떤 처리
~
throw; // 예외를 다시 던진다.
}
잘못된 코드
try {
~
} catch (FildNotFoundException ex) {
// 예외 정보를 사용한 어떤 처리
~
throw ex; // 이렇게 작성하면 안 된다.
}
예외의 스택 트레이스 정보가 사라져서 디버그하기 어려워진다.
.NET 프레임워크 클래스 중에는 Dispose 메서드를 호출하여 사용이 끝난 리소스를 정리해야하는 경우가 있는데,
IDisposable 인터페이스를 구현한 클래스가 여기에 해당한다. 파일, 데이터베이스, 네트워크처럼 외부의 자원에 접근하는 클래스가 대표적인 예이다.
using (var stream = new SteamReader(filePath)) {
var texts = stream.ReadToEnd();
// 읽어 들인 데이터를 여기서 처리한다.
}
이렇게 코딩하면 using을 빠져나갈 때 자동으로 Dispose 메서드가 호출된다. IDisposable 인터페이스를 구현하는 클래스를 사용할 때는 using 문을 사용해 리소스(자원)를 확실히 정리해야한다.
잘못된 코드(예전 방식)
StreamReader steam = new StreamReader(filePath);
try {
string texts = stream.ReadToEnd();
// 읽어 들인 데이터를 여기서 처리한다.
} finally {
steam.Dispose(); // 마지막에 Dispose를 호출해서 정리한다.
}
인수가 다른 생성자를 정의하려면 this 키워드를 사용해 코드를 공유할 수 있는 경우가 있다. ": this(~)"를 사용하면 생성자 본문을 처리하기에 앞서 오버로드된 다른 생성자를 생성할 수 있다.
class AppVersion {
~
public AppVersion(int major, int minor)
: this(major, minor, 0, 0) {
}
public AppVersion(int majer, int minor, int revision)
: this(major, minor, revision, 0) {
}
public Version(int majer, int minor, int build, int revision) {
Major = major;
Minor = minor;
Build = build;
Revision = revision;
}
~
}
잘못된 코드
class AppVersion {
~
public AppVersion(int major, int minor) {
Major = major;
Minor = minor;
Build = 0;
Revision = 0;
}
public AppVersion(int majer, int minor, int build) {
Major = major;
Minor = minor;
Build = build;
Revision = 0;
}
public Version(int majer, int minor, int build, int revision) {
Major = major;
Minor = minor;
Build = build;
Revision = revision;
}
~
}
c# 4.0 이후에는 옵션인수를 사용할 수 있으니 이 방법을 사용해도 좋다.
class AppVersion {
~
public AppVersion(int major, int minor, int build = 0, int revision = 0) {
Major = major;
Minor = minor;
Build = build;
Revision = revision;
}
~
}
this 키워드를 사용하는 4가지 경우
- 자신의 인스턴스를 참조할 때 사용한다.
- 인덱서를 정의할 때 사용한다. (딕셔너리와 엮어서 사용)
- 확장 메서드의 첫 인수의 수식자로 사용한다.
- 한 클래스 내에서 다른 생성자를 호출할 때 사용한다.
두 문자열의 내용이 같은지 조사하려면 == 연산자를 사용한다.
if(str1 == str2)
Console.WriteLine("일치합니다.");
다른 객체를 참조하고 있어도 문자열의 내용이 같다면 동일한 것이라고 판단한다.
대문자와 소문자를 구분하지 않으려면 String.Compare라는 정적 메서드를 사용한다.
var str1 = "Windows";
var str2 = "WINDOWS";
if (String.Compare(str1, str2, true) == 0)
Console.WriteLine("같다.");
else
Console.WriteLine("같지 않다.");
세 번째 인수를 true로 지정하면 대/소문자 구분 없이 비교할 수 있다.
C# 4.0 이후 버전에서는 명명된 인수를 사용해 다음과 같이 쓰면 읽기 쉬운 코드가 된다.
if(String.Compare(str1, str2, ignoreCase:true) == 0)
이렇게 하면 true가 무엇을 의미하는지를 명확하게 알 수 있으므로 코드를 읽기 쉬워지고 주석을 쓰지 않아도 된다.
그 외에도 언어, 국가, 지역, 달력과 같은 정보를 나타내는 클래스인 CultureInfo 클래스와 명명된 인수를 사용해서 히라가나/카타카나 구분 없이 비교, 전각/반각 구별없이 비교등이 가능하다.
CompareOptions.IgnoreKanaType, CompareOptions.IgnoreWidth
문자열이 null인지 빈 문자열인지 조사하려면 String 클래스에 포함된 IsNullOrEmpty 메서드를 사용한다.
if(String.IsNullOrEmpty(str))
Console.WriteLine("null 또는 빈 문자열입니다.");
다음과 같은 코드로도 동일한 결과를 얻을 수 있지만 일반적으로 이렇게 작성하지 않으며, 장황해지기도 하며 How관점에서 조사하기에 옳지 않다.
잘못된 코드
if(str == null || str == "")
Console.WriteLine("null 또는 빈 문자열입니다.");
if(str == null || str.Length == 0)
Console.WriteLine("null 또는 빈 문자열입니다.");
str이 null이 아님이 확실하다면 아래처럼 작성해도 좋다. (null이라면 System.NullReferenceException 예외 발생하니 주의)
if(str == String.Empty)
Console.WriteLine("빈 문자열입니다.");
String.Empty 대신 "" 를 쓰는 것은 개인의 취향 문제이다. (""를 많이써도 메모리에 확보되는 것은 한 개뿐이다.)
.NET 프레임워크 4 이후에는 빈 문자로만 구성된 문자열도 판정의 대상에 포함하고 싶다면 IsNullOrWhiteSpace 메서드를 사용하면 된다.
if(String.IsNullOrWhiteSpace(str)) // null, "", " " 모두 true
~
StartsWith 메서드를 사용하면 인수에 전달된 부분 문자열로 시작되는지 조사할 수 있다.
if(str.StratsWith("Visual")) {
Console.WriteLine("Visual로 시작됩니다.");
}
결과는 같지만 아래와 같이 코딩하면 '무엇을 하고 싶은 것인가'라는 의도를 코드에서 명확하게 보여줄 수 없어서 옳지 않다.
잘못된 코드
if (str.IndexOf("Visual") == 0) { ~ }
마찬가지로 EndsWtih 끝나는 부분을 체크할 때 쓰면 된다.
Contains 메서드를 사용한다.
if (str.Contains("Program")) {
Console.WriteLine("Program이 포함돼 있습니다.");
}
마찬가지로 IndexOf 메서드를 사용해서 작성하는 방법은 옳지 않다.
문자를 체크할 때는 LINQ에 있는 Contains 메서드를 사용해 가능하다.
using System.Linq;
~
var target = "The quick brown fox jumps over the lazy dog.";
var contains = target.Contain('b');
의도를 직관적으로 알 수 없는 아래와 같은 코드.
잘못된 코드
var target = "The quick brown fox jumps over the lazy dog.";
var contains = target.IndexOf('b') >= 0;