C#을 한 번 훑어보면서 익숙하지 않거나 다른언어와 헷갈릴만한 부분만 추려봤다. 이런게 있다고 언급하는 선이기 때문에, 모든 정보를 얻으려 하지말고 관련 키워드를 이용해 검색해보기 바란다.
foreach
대부분 다 C/C++하고 같은데, 다른 언어에서 익숙한 foreach문이 추가로 있다. 추가로 있다고 하기에 좀 민망하긴 한데, C++ 11에도 추가되어 지원하고 있는 문법이다. https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/foreach-in 참조.
var fibNumbers = new List<int> { 0, 1, 1, 2, 3, 5, 8, 13 };
int count = 0;
foreach (int element in fibNumbers)
{
count++;
Console.WriteLine($"Element #{count}: {element}");
}
Console.WriteLine($"Number of elements: {count}");
format string
Kotlin, Python에서 자주쓰던 format string을 지원한다. 옛날 방식은 생략하고 최근 방식만 언급하면, 3개가 다 유사하다. 문자열앞에 특수문자를 붙이는데, Python은 ‘f’, Kotlin은 따로 붙이는게 없고, C#은 ‘$’를 붙인다. 스트링 내부에서는 3개다 동일하게 ‘{ }’로 묶인 부분안에 expression이 들어갈 수 있고, 처리된 결과물이 문자열로 쓰인다. https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/exploration/interpolated-strings 참조.
var name = "<name>";
Console.WriteLine($"Hello, {name}. It's a pleasure to meet you!");
format string이 가독성이 좋을지 모르지만, 여전히 string builder 형태가 많이 쓰인다. 그냥 ‘+’ 연산으로 스트링과 expression들을 더해 새로운 스트링을 만드는 형태다.
string string1 = "Today is " + DateTime.Now.ToString("D") + ".";
Console.WriteLine(string1);
string string2 = "This is one sentence. " + "This is a second. ";
string2 += "This is a third sentence.";
Console.WriteLine(string2);
method named arguments
Kotlin, Python 다 함수의 인자이름을 명시해서 사용할 수 있다. C#도 마찬가지긴 한데, 앞의 두 언어가 ‘name’=’value’ 형태를 쓰는거에 반해서 ‘name’ : ‘value’ 형태로 넘긴다. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments 참조.
PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
Access Modifiers
private, protected, public은 익숙한데, internal 이 추가로 존재한다. 게다가 두개의 콤비네이션도 가능하다? protected internal, private protected 처럼. 응?
properties
처음엔 좀 혼란스러운 개념이었는데, field 와 property를 구분해서 이해해야 한다. field는 익히 알고 있듯이 클래스내의 variable 들이다. OOP에선 인캡슐레이션을 구현하기 위해 이 fields를 직접 노출하지 않고 getter/setter를 통해서만 접근하도록 한다. 하지만, 이렇게 구현되면 매번 사용자가 getter/setter를 호출하는게 번거로우므로, property라는 getter/setter 셋을 제공해서 field를 직접 읽고 쓰듯이 사용가능하게 해준다.
코틀린도 property를 full로 사용하면 getter/setter를 다 사용해 다음처럼 된다.
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // parses the string and assigns values to other properties
}
실제 값을 저장하는 field를 backing field라 부르는데, Kotlin에서는 필요한 경우 자동으로 생성해준다. Kotlin에서는 setter에서 이 field에 접근가능하도록 ‘field’ 키워드를 사용한다.
var counter = 0 // Note: the initializer assigns the backing field directly
set(value) {
if (value >= 0) field = value
}
이렇게 사용하지 못하는 경우, 직접 backing property를 정의해서 사용가능하다.
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // Type parameters are inferred
}
return _table ?: throw AssertionError("Set to null by another thread")
}
C#도 이와 유사하다.
class Person
{
private string name; // field
public string Name // property
{
get { return name; } // get method
set { name = value; } // set method
}
}
C#에서는 backing field를 자동생성하게 하려면, 다음처럼 비어있는 getter/setter를 만들어주면 된다.
public string Name // property
{ get; set; }
sealed class
Kotlin에도 sealed class가 있지만, 많이 다르다. Kotlin에선 sealed class 자체는 abstract로 당연히 상속이 가능하고 상속해서 사용한다. 다만, seald class가 정의된 파일 안에서만 가능하다.
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
이런게 왜 필요한지 의문이 들 수 있다. 나도 그러니까. 실 사용예로 들면, 보다 유연한 enum형태의 사용이 가능하다. 기존 enum은 단일 타입 객체의 나열이었다면, sealed class를 사용하는 경우 각각 다른 객체들이 enum처럼 사용가능해진다. https://android.jlelse.eu/kotlin-what-is-a-sealed-classe-1e535c416519 참고.
C#에서는 private 처럼 사용된다. ‘sealed’ 키워드를 클래스에 붙이면 상속이 불가능해진다.
sealed class Vehicle
{
...
}
class Car : Vehicle // Error
{
...
}
private과 차이가 뭐냐고 할 수 있는데, C# 에서 nested class가 아닌이상, private class는 사용할 수가 없다. 이에 대한 대안 정도로 생각해도 될 거 같다.
preprocessor directives
C/C++에서 봤던 바로 그것. ‘#’으로 시작하며, #define, #undef, #if, #else, #endif 등이 있다. 자세한건 https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/preprocessor-directives/ 참조.
#define DEBUG
//#define TRACE
#undef TRACE
using System;
public class TestDefine
{
static void Main()
{
#if (DEBUG)
Console.WriteLine("Debugging is enabled.");
#endif
#if (TRACE)
Console.WriteLine("Tracing is enabled.");
#endif
}
}
Collections
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/collections 참조.
다른언어와 비슷하게 Collections를 제공한다. map대신 Dictionary가 쓰이는게 특이하며, Set이 안보이는데, HashSet<T>, SortedSet<T> 의 구현버전이 존재한다. 특이하다고 할 수는 없는데, 코틀린에 기본으로 없다보니 언급하자면 Stack<T>과 Queue<T>도 있다.
멀티쓰레드 환경에서 사용할 collection은 따로 존재한다. https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent?view=netframework-4.8 참조.
여기서부터 본격적으로 C#만의 특별한 내용같기도 하다. 전혀 익숙하지 않은 내용들이다. 그다지 직관적이지 않으니, 차근 차근 알아가야 할듯.
Attributes
메타 데이타를 부여하는 방법. 생각보다 좀 복잡하다. Unity에 대해서만 한정한다면, 잘 모르고 써오던 다음 코드들이 attributes들이다. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/ 참조.
조금 예전 문서지만 unity 관련해서는 https://unity3d.college/2017/05/22/unity-attributes/ 참조.
[SerializeField]
private int velocity = 0;
Delegates
함수에 대한 레퍼런스가 delegate이다. Kotlin이나 Python에선 함수가 first class이기 때문에 이런거 없이 자연스럽게 구현된다. 보통 events 와 call-back 구현에 사용되며, 모든 delegates는 System.Delegate에서 상속받음. https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/delegates/ 참조.
public delegate void Del(string message);
// Create a method for a delegate.
public static void DelegateMethod(string message)
{
Console.WriteLine(message);
}
// Instantiate the delegate.
Del handler = DelegateMethod;
// Call the delegate.
handler("Hello World");
예에서 보이듯이 약간 typedef 같은 형태다. 함수의 선언형태로 타입을 정의하고, 정의한 타입변수에 정의된 함수를 동적으로 할당한다.
Events
익숙한 Observer pattern, 다른말로는 publisher-subscripber 모델을 언어레벨에서 지원하는 내용이다. 클래스나 객체에 변경사항이 있으면, 다른 클래스나 객체에 변경사항을 알려준다. 콜백과 같은 형태로 delegate를 이용한다. https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/events/ 참조. 사용법은 좀 복잡해 보인다.
- 이벤트 핸들러로 쓰일 delegate를 선언한다. sender와 arguement 두개의 인자를 갖는다.
public delegate void EventHandler(object sender, EventArgs e);
- Publisher에서 이벤트를 정의하고, 변경사항이 있을 때, 해당 이벤트를 체크해서 값이 있으면 함수 형태로 실행한다.
public class Publisher: ArrayList
{
public event EventHandler ProdcutAddedInfo;
protected virtual void OnChanged(EventArgs e)
{
if (ProdcutAddedInfo != null) ProdcutAddedInfo(this, e);
}
public override int Add(Object product)
{
int added = base.Add(product);
OnChanged(EventArgs.Empty);
return added;
}
public override void Clear()
{
base.Clear();
OnChanged(EventArgs.Empty);
}
public override object this[int index]
{
set
{
base[index] = value;
OnChanged(EventArgs.Empty);
}
}
}
예제에서 OnChanged()에서 이벤트가 비었는지 null 여부로 체크하고, 호출하는 부분을 볼 수 있다.
- subscriber는 publisher의 이벤트에 이벤트 핸들러를 등록한다. 등록은 ‘+=’ 연산자로 손쉽게 된다. 필요한경우, 등록된 핸들러를 해제할 수 있다. 해제는 ‘-=’ 연산자로 가능하다.
public class Subscriber
{
private Publisher publishers;
public Subscriber(Publisher publisher)
{
this.publishers = publisher;
publishers.ProdcutAddedInfo += publishers_ProdcutAddedInfo;
}
void publishers_ProdcutAddedInfo(object sender, EventArgs e)
{
if (sender == null)
{
Console.WriteLine("No New Product Added.");
return;
}
Console.WriteLine("A New Prodct Added.");
}
public void UnSubscribeEvent()
{
publishers.ProdcutAddedInfo -= publishers_ProdcutAddedInfo;
}
}
예제에서 publisher를 생성자 인자로 받아, delegate와 같은 타입의 멤버함수를 이벤트에 ‘+=’ 연산자로 등록한다. UnSubscribeEvent()에선 이벤트 핸들러를 제거하는걸 보여준다.
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber(publisher);
publisher.Add(new Product
{
ProductName = "Complan", Price = 20
});
publisher.Add(new Product
{
ProductName = "Horlics", Price = 120
});
publisher.Add(new Product
{
ProductName = "Boost", Price = 200
});
subscriber.UnSubscribeEvent();
Console.ReadKey();
}
}
Main에서 Publisher의 Add()를 호출할 때마다, publisher.OnChange() 가 불리고, OnChange()에선 event를 체크하는데 subscriber가 생성될 때 등록해놓은 이벤트 핸들러가 있으므로 불러주게 되고, 최종적으로 subscriber.publishers_productAddedInfo() 가 호출된다.
예제 소스 : https://www.c-sharpcorner.com/UploadFile/1c8574/events-in-C-Sharp/ 인용. 익숙하지 않아서 잘 읽히는 예제를 가져왔다.
Indexers
너무 새로운 개념인데, class, 를 배열처럼 사용하게 해준다. indexer의 선언은 다음과 같다.
public int this[int index] // Indexer declaration
{
// get and set accessors
}
사용예는 다음과 같다.
class TempRecord
{
// Array of temperature values
private float[] temps = new float[10] { 56.2F, 56.7F, 56.5F, 56.9F, 58.8F,
61.3F, 65.9F, 62.1F, 59.2F, 57.5F };
// To enable client code to validate input
// when accessing your indexer.
public int Length
{
get { return temps.Length; }
}
// Indexer declaration.
// If index is out of range, the temps array will throw the exception.
public float this[int index]
{
get
{
return temps[index];
}
set
{
temps[index] = value;
}
}
}
class MainClass
{
static void Main()
{
TempRecord tempRecord = new TempRecord();
// Use the indexer's set accessor
tempRecord[3] = 58.3F;
tempRecord[5] = 60.1F;
// Use the indexer's get accessor
for (int i = 0; i < 10; i++)
{
System.Console.WriteLine("Element #{0} = {1}", i, tempRecord[i]);
}
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
Element #0 = 56.2
Element #1 = 56.7
Element #2 = 56.5
Element #3 = 58.3
Element #4 = 58.8
Element #5 = 60.1
Element #6 = 65.9
Element #7 = 62.1
Element #8 = 59.2
Element #9 = 57.5
*/
사용예를 보면, attributes가 생각난다. 객체에 배열과 같이 사용하는 인터페이스를 만들어 준다고 생각하면 될 거 같다.
Lambda
C# 에서도 lambda expression을 지원한다. Kotlin과 크게 다르지 않은데, ‘->’ 화살표 대신 ‘=>’ 화살표를 사용한다. …이런건 좀 하나만 쓰자. 피곤하다고 ㅋ https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions 참조.
Action line = () => Console.WriteLine();
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
여러줄의 코드일 경우 ‘{ }’로 묶어준다.
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
async, await
비동기 프로그래밍에 사용되는 것들이다. https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/concepts/async/ 참조.
지쳤다. 일단 여기까지만 알아보자. 비동기 프로그래밍만 따로 정리해서 올릴정도로 양이 많다.
LINQ(Language-Integrated Query)
data query문은 언어문법과 무관하게 스트링으로 처리되곤 했다. LINQ는 언어 차원에서 query expression을 지원한다. 말을 옮기긴 했지만, 제대로 공부해본적도 써본적도 없는 내용이라 언급만 하고 넘어간다. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/ 참조.
가볍게 정리하려고 시작했는데, 왤케 어려운 개념이 많은건지 모르겠다. 아 피곤한데 좀.