- 파일 정보와 디렉토리 정보 다루기
- 파일을 읽고 쓰기 위해 알아야 할 것들
- 이진 데이터 처리를 위한 Binary Writer / Binary Reader
- 텍스트 파일 처리를 위한 Stream Writer / Stream Reader
- 객체 직렬화하기
[ 파일 정보와 디렉토리 정보 다루기 ]
[ 파일과 디렉토리 ]
- 파일은 컴퓨터 저장 매체에 기록되는 데이터의 묶음 .
- 디렉토리는 파일이 위치하는 주소로 파일을 담는다는 의미에서 폴더라고도 부른다 .
- .Net은 파일과 디렉토리 정보를 손쉽게 다룰 수 있도록 System.IO 네임스페이스에 다음 클래스를 제공한다 .
[ 파일 관련 클래스 ]
클래스 | 설명 |
File | 파일의 생성,복사,삭제,이동,조회를 처리하는 정적 메소드를 제공 . |
FileInfo | File 클래스와 하는 일은 동일하지마나 정적 메소드 대신 인스턴스 메소드를 제공한다 . |
Directory | 디렉토리의 생성,삭제,이동,조회를 처리하는 정적 메소드를 제공한다 . |
DirectoryInfo | Directory 클래스와 하는 일은 동일하지만 정적 메소드 대신 인스턴스 메소드를 제공한다 . |
- 하나의 파일에 대해 한두가지 작업 => File 정적 메소드를 이용
- 하나의 파일에 대해 여러작업 수행 => FileInfo 인스턴스 메소드를 이용
[ File / FileInfo 메서드와 프로퍼티 ]
기능 | File | FileInfo |
생성 | FileStream fs = File.Create("a.dat"); | FileInfo file=new FileInfo("a.dat"); FileStram fs = file.Create(); |
복사 | File.Copy("a.dat","b.dat"); | FileInfo src = new FileInfo("a.dat"); FileInfo dst = src.CopyTo("d.dst"); |
삭제 | File.Delete("a.dat"); | FileInfo file = new FileInfo("a.dat"); file.Delete(); |
이동 | File.Move("a.dat","b.dat"); | FileInfo file=new FileInfo("a.dat"); file.MoveTo("b.dat"); |
존재 여부 확인 | if(File.Exsits("a.dat")) // ... |
FileInfo file = new FileInfo("a.dat"); if(file.Exsits) // ... |
속성조회 | Console.Write(File.GetAttributes("a.dat")); | FileInfo file = new FileInfo("a.dat"); Console.Write(file.Attributes); |
[ Directory / DirectoryInfo 메서드와 프로퍼티 ]
기능 | Directory | DirectoryInfo |
생성 | DirectoryInfo dir=Directory.CreateDirectory("a"); | DirectoryInfo dir = new DirectoryInfo("a"); dir.Create(); |
삭제 | Directory.Delete("a"); | DirectoryInfo dir = new DirectoryInfo("a"); dir.Delete(); |
이동 | Directory.Move("a","b"); | DirecoryInfo dir = new DirectoryInfo("a"); dir.MoveTo("b"); |
존재 여부 확인 | if(Directory.Exsits("a.dat")) // ... |
DirectoryInfo dir = new DirectoryInfo("a") if(dir.Exsits) // ... |
속성조회 | Console.WriteLine(Dircetory.GetAttributes("a")) | DirectoryInfo dir = new DirectoryInfo("a") Console.WriteLine(dir.Attributes); |
하위 디렉토리 조회 | string[]dirs = Directory.GetDirectories("a"); | DirectoryInfo dir = new DirectoryInfo("a"); string[]dirs = dir.GetDirectories(); |
하위 파일 조회 | string[]files = Directory.GetFiles("a"); | DirectoryInfo dir = new DirectoryInfo("a"); dir.GetFiles(); |
[ 예제 프로그램 : 디렉토리 / 파일 정보 조회하기 ]
using System;
using System.Linq;
using System.IO;
namespace ConsoleApp;
class MainApp
{
static void Main(string[] args)
{
string directory = args.Length < 1 ? "." : args[0];
Console.WriteLine($"{directory} directory Info");
Console.WriteLine("- Directories :");
var directories = (from dir in Directory.GetDirectories(directory)
let info = new DirectoryInfo(dir)
select new
{
Name = info.Name,
Attributes = info.Attributes
}).ToList();
foreach (var d in directories)
Console.WriteLine($"{d.Name} : {d.Attributes}");
Console.WriteLine("- Files : ");
var files = (from file in Directory.GetFiles(directory)
//let은 LINQ안에서 변수를 만든다 . var 와 같은 역할
let info = new FileInfo(file)
select new
{
Name=info.Name,
FileSize=info.Length,
Attributes = info.Attributes
}).ToList();
foreach (var f in files)
Console.WriteLine($"{f.Name} : {f.FileSize} , {f.Attributes}");
}
}
[ 예제 프로그램 : 디렉토리 / 파일 생성하기 ]
using System;
using System.Linq;
using System.IO;
namespace ConsoleApp;
class MainApp
{
static void OnWrongPathType(string type)
{
Console.WriteLine($"{type}is wrong type");
return;
}
static void Main(string[] args)
{
if(args.Length == 0)
{
Console.WriteLine(
"Usage : Test.exe <Path> [Type : File/Direcoty]");
return;
}
string path = args[0];
string type = "File";
if(args.Length > 1)
type = args[1];
//이미 존재한다면 최종수정시간만 갱신
if(File.Exists(path)||Directory.Exists(path))
{
if (type == "File")
File.SetLastWriteTime(path, DateTime.Now);
else if(type=="Directory")
Directory.SetLastWriteTime(path,DateTime.Now);
else
{
OnWrongPathType(path);
return;
}
Console.WriteLine($"Updated {path} {type}");
}
//생성
else
{
if (type == "File")
File.Create(path).Close();
else if(type=="Directory")
Directory.CreateDirectory(path);
else
{
OnWrongPathType(path);
return;
}
Console.WriteLine($"Create {path} {type}");
}
}
}
[ 파일을 읽고 쓰기 위해 알아야 할 것들 ]
[ 스트림 ]
- 파일을 다룰때 스트림은 데이터가 흐르는 통로 .
- 메모리에서 하드디스크로 데이터를 옮길 때 먼저 스트림을 만들어 둘 사이를 연결, 메모리에 있는 데이터를
바이트 단위로 하드디스크로 옮긴다 .
[ 순차 접근 방식 ]
- 스트림(데이터의 흐름)을 이용하여 파일을 다룰 때는 처음부터 끝까지 순서대로 읽고 쓰는것이 보통 (순차접근방식)
- 스트림의 구조는 네트워크/데이터 백업 장치의 입/출력 구조와도 통한다 .
- 스트림 이용시 파일이 아닌 네트워크를 향해 데이터를 흘려보내거나 읽을수 있고 , 테이프 백업 장치를 통해 데이터를 기록하거나 읽을 수 도 있다.
- 즉 , 첫번째 바이트를 읽어야 두번째를 읽을 수 있다 .
[ 임의 접근 방식 ]
- 하드디스크는 데이터의 흐림이 단방향성을 가진 네트워크/자기 테이프 장치와 다르다 .
- 암과 헤드를 움직여 디스크의 어떤 위치에 기록된 데이터라도 즉시 찾을 수 있다 . (임의 접근 방식)
- 1MB 크기의 파일에서 786 byte 번째에 위치한 데이터를 읽을때 , 앞을 다 읽지 않아도 원하는 위치에 접근 가능하다 .
[ System.IO.Stream 클래스 ]
[ Stream 클래스 ]
- 그 자체로 입력 스트림/출력 스트림의 역할을 모두 할 수 있다 .
- 파일을 읽고 쓰는 방식인 순차접근 / 임의접근 방식 모두 지원한다 .
- Stream은 추상 클래스이기에 파생 클래스를 사용해야 한다 .
- 이는 다양한 매체 / 장치들에 대한 파일 입출력을 스트림 모델 하나로 다루기 위함이다 . ex)File Stream / Network Stream
[ FileStream 인스턴스 생성하기 ]
Stream stream1 = new FileStream("a.dat",FileMode.Create); //새파일 생성
Stream stream2 = new FileStream("b.dat",FileMode.Open); //파일 열기
Stream stream3 = new FileStream("c.dat",FileMode.OpenOrCreate); //새파일 열거나 없으면 생성
Stream stream4 = new FileStream("d.dat",FileMode.Truncate); //파일 비워서 열기
Stream stream5 = new FileStream("e.dat",FileMode.Append); //덧붙이기 모드로 열기
[ 파일 쓰기 ]
public override void Write(
byte[]array, //쓸 데이터가 담겨 있는 byte 배열
int offset, //byte 배열 내 시작 오프셋
int count //기록할 데이터의 총 길이 (단위는 바이트)
);
public override void WriteByte(byte value);
long someValue = 0x123456789ABCDEF0;
// 1)파일 스트림 생성
Stream outStream = new FileStream("a.dat",FileMode.Create);
// 2)SomeValue(long 형식)을 byte 배열로 변환
byte[] wBytes = BitConverter.GetBytes(someValue);
// 3)변환한 byte 배열을 파일 스트림을 통해 파일에 기록
outStream.Write(wBytes,0,wBytes.Length);
// 4)파일 스트림 닫기
outStream.Close();
- FileStream 클래스는 파일에 데이터를 기록하기 위해 Stream 클래스로부터 물려받은 두가지를 오버라이딩 한다 .
- 위의 Write 메서드는 각종 데이터 형식을 byte로 변환해주는 BitConverter클래스를 통해 변환후 기록한다 .
- 임의 형식의 데이터를 byte 배열로 변환 / byte 배열에 담겨 있는 데이터를 다시 임의 형식의 데이터로 변환해준다 .
[ 파일 읽기 ]
public override int Read(
byte[]array, //읽을 데이터를 담을 byte 배열
int offset, //byte 배열 내 시작 오프셋
int count //읽을 데이터의 최대 바이트 수
);
public override int ReadByte();
byte[] rbytes = new Byte[8];
// 1)파일 스트림 생성
Stream inStream = new FileStream("a.dat",FileMode.Open);
// 2)rBytes의 길이만큼 (8바이트)데이터를 읽어 rbytes에 저장
inStream.Read(rBytes,0,rBytes.Length);
[ 예제 프로그램 - 하나의 데이터 기록하기 ]
using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
namespace ConsoleApp;
class MainApp
{
static void Main(string[] args)
{
long SomeValue = 0x123456789ABCDEF0;
Console.WriteLine("{0,-1} : 0x{1:X16}", "Original Data", SomeValue);
//쓰기
Stream outStream = new FileStream("a.dat", FileMode.Create);
byte[]wBytes = BitConverter.GetBytes(SomeValue);
Console.Write("{0,-13} : ", "Byte Array");
foreach (byte b in wBytes)
Console.Write("{0:x2}", b);
Console.WriteLine();
outStream.Write(wBytes,0,wBytes.Length);
outStream.Close();
//읽기
Stream inStream = new FileStream("a.dat", FileMode.Open);
byte[] rbytes = new byte[8];
int i = 0;
while(inStream.Position<inStream.Length)
rbytes[i++]=(byte)inStream.ReadByte();
long readValue = BitConverter.ToInt64(rbytes, 0);
Console.WriteLine("{0,-13} : 0x{1:x16}", "Read Data", readValue);
inStream.Close();
}
}
- long 형식으로부터 변환된 ByteArray의 순서가 역순임을 볼 수 있다 .
- 이는 CLR이 지원하는 바이트 오더가 데이터의 낮은 주소부터 기록하는 리틀 엔디안 방식이기에 나타난 현상이다 .
- 자바의 가상머신은 빅 엔디안 바이트 오더를 지원한다 .
- ARM과 x86 계열의 CPU들은 리틀 엔디안 방식으로 동작하지만 PowerCPU / Sparc 계열의 CPU는 빅엔디안으로 동작
- 즉 , 서로 다른 바이트 오더 방식을 사용하는 시스템 투성이기에 C#의 프로그램을 다른 시스템에서 읽도록 하려면 바이트 오더의 차이를 반드시 고려해야 한다 .
[ 순차접근 ]
- Stream 클래스에는 Positioin이라는 프로퍼티가 존재한다 .
- 이는 현재 스트림의 읽는 위치 / 쓰는 위치를 나타낸다 .
- 여러개의 데이터 여러개를 기록하는 일은 Write() / WriteByte()메소드를 차례차례 호출하는 것으로 충분하다 .
- 이렇게 파일을 순차적으로 쓰거나 읽는 방식을 순차접근이라고 한다 .
[ 임의접근 ]
- 파일 내의 임의의 위치에 Positioin이 위치하게 할 수 있다 .
- 임의 접근은 Seek()메소드 호출 / Position 프로퍼티에 직접 원하는 갑 대입시 지정한 위치로 점프 , 읽기/쓰기가 가능하다
[ 예제 프로그램 - 순차접근 / 임의접근 ]
using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
namespace ConsoleApp;
class MainApp
{
static void Main(string[] args)
{
Stream outStream = new FileStream("a.dat", FileMode.Create);
Console.WriteLine($"Position : {outStream.Position}");
outStream.WriteByte(0x01);
Console.WriteLine($"Position : {outStream.Position}");
outStream.WriteByte(0x02);
Console.WriteLine($"Position : {outStream.Position}");
outStream.WriteByte(0x03);
Console.WriteLine($"Position : {outStream.Position}");
outStream.Seek(5, SeekOrigin.Current);
Console.WriteLine($"Position : {outStream.Position}");
outStream.WriteByte(0x04);
Console.WriteLine($"Position : {outStream.Position}");
outStream.Close();
}
}
- 0 ,1, 2 번지까지 순차적으로 기록하다 3,4,5,6,7 5개의 번지를 건너뛰어 8번지에 기록하였다 .
[ 힘든 파일 입출력 ]
- 메모리의 구조와 데이터를 저장할 장치의 구조가 상이하기에 생긴 문제이다 .
- 그나마 C# 클래스가 디스크 / 네트워크 등에 데이터를 기록하고 읽는 방법을 Stream 클래스로 추상화했기에
같은 프로그래밍 모델을 이용 할 수 있다 .
- FileStream을 통해 Stream 파일 처리 모델을 익혀두면 네트워크 프로그래밍에 도움이 될 것이다 .
[ 실수를 줄여주는 using 선언 ]
[ 대표적인 실수 ]
//파일 스트림 닫기 - 프로그래머가 주로 빼먹는 부분
outStream.Close();
//1 . using 선언
{
using Stream outStream = new FileStream("a.dat",FileMode.Create);
// 나머지 동작들
}
//using 선언을 통해 생성된 객체는 코드블록이 끝나면서 outStream.Dispose() 호출
//2 . using 선언문 아래에 코드 블록을 만들어 자원의 수명을 세부적으로 조절 .
using (Stream outStream = new FileStream("a.dat",FileMode.Create))
{
//나머지 동작들
}
//using 선언을 통해 생성된 객체는 코드블록이 끝나면서 outStream.Dispose() 호출
- 코드 블록의 마지막에서 Dispose()메소드가 호출되도록 하는 using 선언은 Close()메소드 호출과 사실상 동일한 코드
- Stream.Close()메소드가 IDisposable 인터페이스에서 상속받은 Dispose()메소드를 호출하기 때문 .
- using 선언은 Stream 객체뿐 아니라 IDispose를 상속해서 Dispose()메소드를 구현하는 모든 객체에 대해 사용가능하다 .
using FS = System.IO.FireStream;
using Stream outStream = new FS("a.dat",FileMode.Create))
{
//세부 동작
}
- using 별칭 지시문을 통해 긴 이름의 클래스를 간단한 별칭으로 사용 가능하다 .
[ 이진 데이터 처리를 위한 BinaryWriter / BinaryReader ]
[ BinaryWriter / Reader ]
- FileStream 클래스는 데이터 저장시 byte / byte 배열 형식으로 변환이 필요한 불편함이 있다 .
- .Net은 불편함을 해소하기 위해 BinaryWriter / BinaryReader 클래스를 제공한다 .
//생성자를 호출하며 FileStream의 인스턴스를 인수로 넘긴다 .
//BinaryWriter 객체는 FileStream 인스턴스가 생성한 스트림에 대해 이진 데이터 기록을 수행
BinaryWriter bw = new BinaryWriter(new FileStream("a.dat",FileMode.Create));
//Write()메소드는 모든 데이터 형식에 대해 오버로딩 됨
bw.Write(32);
bw.Write("Good");
bw.Write(3.15);
bw.Close();
BinaryReader br = new BinaryReader(new FileStream("a.dat",FileMode.Open));
int a= br.ReadInt32();
string b = br.ReadString();
double c = br.ReadDouble();
br.Close();
[ 예제 ]
using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
namespace ConsoleApp;
class MainApp
{
static void Main(string[] args)
{
using (BinaryWriter bw = new BinaryWriter(new FileStream("a.dat",FileMode.Create)))
{
bw.Write(int.MaxValue);
bw.Write("Good Morning");
bw.Write(uint.MaxValue);
bw.Write("안녕하세요");
bw.Write(double.MaxValue);
}
//만약 위와 같은 스타일의 using 선언이 아니었다면 열려 있는 파일을 다시 여는것과 같음
using BinaryReader br = new BinaryReader(new FileStream("a.dat", FileMode.Open));
Console.WriteLine($"File Size : {br.BaseStream.Length} bytes");
Console.WriteLine($"{br.ReadInt32()}");
Console.WriteLine($"{br.ReadString()}");
Console.WriteLine($"{br.ReadUInt32()}");
Console.WriteLine($"{br.ReadString()}");
Console.WriteLine($"{br.ReadDouble()}");
}
}
- FileStream 만으로 데이터를 기록시 BitConverter를 이용 , 각 데이터를 바이트 단위로 나눠 저장했음 .
- BinaryWriter는 각 데이터 타입을 알아서 바이트 단위로 저장해준다 .
- 문자열 저장시 문자열의 길이를 저장할 데이터의 가장 첫번째 바이트에 바이트에 저장한후 그 뒤에 데이터를 저장한다 .
[ 텍스트 파일 처리를 위한 StreamWriter / StreamReader ]
[ 텍스트 파일 ]
- 텍스트 파일은 구조는 간단하지만 활용도가 높은 파일 형식이다 .
- .Net은 텍스트 파일 을 쓰고 읽을수 있게 StreamWriter / StreamReader를 제공한다 .
- Stream이 NetworkStream 이라면 네트워크를 통해 텍스트 데이터를 내보내거나 읽어들인다 .
- Stream이 FileStream 이라면 파일로 텍스트 데이터를 내보내거나 읽어들인다 .
using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
namespace ConsoleApp;
class MainApp
{
static void Main(string[] args)
{
using (StreamWriter sw = new StreamWriter(new FileStream("a.txt",FileMode.Create)))
{
sw.WriteLine(int.MaxValue);
sw.WriteLine("Good Morning");
sw.WriteLine(uint.MaxValue);
sw.WriteLine("안녕하세요");
sw.WriteLine(double.MaxValue);
}
//만약 위와 같은 스타일의 using 선언이 아니었다면 열려 있는 파일을 다시 여는것과 같음
using (StreamReader sr = new StreamReader(new FileStream("a.txt", FileMode.Open)))
{
Console.WriteLine($"File Size : {sr.BaseStream.Length} bytes");
//EndOfStream 프로퍼티는 스트림의 끝에 도달했는지 알려준다 .
while(sr.EndOfStream==false)
{
Console.WriteLine(sr.ReadLine());
}
}
}
}
[ 객체 직렬화 하기 ]
[ 직렬화 ]
- 프로그래머가 직접 정의한 클래스나 구조체 같은 복합 데이터 형식은 스트림에 읽고 쓰는 것을 지원하지 않는다 .
- 읽고 쓰려면 그 형식이 가진 필드의 값을 저장할 순서를 정한후 , 순서대로 저장하고 읽는 코드를 작성해야 한다 .
- 위 작업은 C#에서 직렬화라는 메커니즘으로 간편하게 제공한다 .
- 직렬화는 객체의 상태(필드에 저장된 값)를 메모리/영구 저장 장치에 저장이 가능한 0과 1의 순서로 바꾸는 것을 말한다 .
[ 직렬화 하기 ]
Stream ws = new FileStream(fileName,FileMode.Create);
NameCard nc = new NameCard();
string jsonString = JsonSerializer.Serialzie<NameCard>(nc);//직렬화
byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonString);
ws.Write(jsonBytes,0,jsonBytes.Length);
- 직렬화 할 프로퍼티를 public 으로 한정한다 .
- JsonSerializer는 System.Text.Json 네임스페이스에 소속 , 객체를 JSON 형태로 직렬화 / 역직렬화 한다 .
[ 역직렬화 하기 ]
Stream rs = new FileStream(fileName,FileMode.Open)
byte[]jsonBytes = new Byte[rs.Length];
rs.Read(jsonBytes,0,jsonBytes.Length);
string jsonString = System.Text.Encoding.UTF8.GetString(jsonBytes);
NameCard nc2 = JsonSerialzier.Desirializer<NameCard>(jsonString)
[ 직렬화 무시하기 ]
class NameCard
{
public string Name{get;set;}
[JsonIgnore]
public int Age{get;set;}
}
- JsonIngore 어트리뷰트로 수식하면 직렬화 될 때 저장되지 않고 , 역직렬화시 복원되지 않는다 .
[ 직렬화 예제 ]
using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
namespace ConsoleApp;
class NameCard
{
public string Name { get; set; }
public string Phone { get; set; }
public int Age { get; set; }
}
class MainApp
{
static void Main(string[] args)
{
var fileName = "a.json";
using(Stream ws = new FileStream(fileName,FileMode.Create))
{
NameCard nc = new NameCard()
{
Name="박상현",
Phone="010-123-4567",
Age=33
};
string jsonString = JsonSerializer.Serialize<NameCard>(nc);
byte[]jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonString);
ws.Write(jsonBytes,0,jsonBytes.Length);
}
using (Stream rs = new FileStream(fileName, FileMode.Open))
{
byte[]jsonBytes = new byte[rs.Length];
rs.Read(jsonBytes,0,jsonBytes.Length);
string jsonString = System.Text.Encoding.UTF8.GetString(jsonBytes);
NameCard nc2 = JsonSerializer.Deserialize<NameCard>(jsonString);
Console.WriteLine($"Name : {nc2.Name}");
Console.WriteLine($"Phone : {nc2.Phone}");
Console.WriteLine($"Age : {nc2.Age}");
}
}
}
[ 컬렉션 직렬화 예제 ]
using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
namespace ConsoleApp;
class NameCard
{
public string Name { get; set; }
public string Phone { get; set; }
public int Age { get; set; }
}
class MainApp
{
static void Main(string[] args)
{
var fileName = "a.json";
using(Stream ws = new FileStream(fileName,FileMode.Create))
{
var list = new List<NameCard>();
list.Add(new NameCard() { Name="박상현",Phone="010-123-4567",Age=33});
list.Add(new NameCard() { Name = "김연아", Phone = "010-3333-4567", Age = 32 });
list.Add(new NameCard() { Name = "장미란", Phone = "010-443-67", Age = 39 });
string jsonString = JsonSerializer.Serialize<List<NameCard>>(list);
byte[]jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonString);
ws.Write(jsonBytes,0,jsonBytes.Length);
}
using (Stream rs = new FileStream(fileName, FileMode.Open))
{
byte[]jsonBytes = new byte[rs.Length];
rs.Read(jsonBytes,0,jsonBytes.Length);
string jsonString = System.Text.Encoding.UTF8.GetString(jsonBytes);
var list2 = JsonSerializer.Deserialize<List<NameCard>>(jsonString);
foreach(NameCard nc in list2)
{
Console.WriteLine($"Name : {nc.Name} , Phone : {nc.Phone} , Age : {nc.Age}");
}
}
}
}
- List를 비롯한 컬렉션들도 직렬화를 지원한다 .
'C# > 이것이 C#이다' 카테고리의 다른 글
[ 이것이 C#이다 ] Chapter 20. WinForm으로 만드는 사용자 인터페이스 (0) | 2024.04.16 |
---|---|
[ 이것이 C#이다 ] Chapter 19. 스레드와 태스크 (0) | 2024.04.12 |
[ 이것이 C#이다 ] Chapter 16 . 리플렉션과 애트리뷰트 (0) | 2024.04.02 |
[ 이것이 C#이다 ] Chapter 15 . LINQ (0) | 2024.03.27 |
[ 이것이 C#이다 ] Chapter 14 . 람다식 (0) | 2024.03.25 |