728x90

이번에 개발한 프로그램이 윈도우 시작시 실행이 되어야 하는데, 레지스트리에 등록을 해서 실행을 하니 관리자 권한이 필요한 프로그램이라 실행이되지 않았다.

 

그래서 작업 스케줄러에 등록하는 방법으로 하였다.

 

trigger를 BootTrigger로 줘야만 부팅 후 LockWorkStation 상태에서 프로그램이 켜질 줄 알았는데, LogonTrigger로 줘도 무방했다. BootTrigger를 주면 오히려 프로그램이 제대로 동작하지 않는 현상이 발생했다.

class WindowsAutoStarter
{
    private const string kTASK_SCHEDULER_NAME = "My App";
    public static void SetAutoStart(bool enable)
    {
        TaskService taskService = new TaskService();
        TaskDefinition taskDefinition = taskService.NewTask();

        if (enable)
        {
            // trrigger
            taskDefinition.Triggers.Add(new LogonTrigger());
            //taskDefinition.Triggers.Add(new BootTrigger());

            // general
            taskDefinition.Principal.DisplayName = "ProgramName";
            taskDefinition.RegistrationInfo.Description = "blahblah";
            taskDefinition.Principal.UserId = string.Concat(Environment.UserDomainName, "\\", Environment.UserName);
            taskDefinition.Principal.LogonType = TaskLogonType.InteractiveToken;
            taskDefinition.Principal.RunLevel = TaskRunLevel.Highest;

            // condition 
            taskDefinition.Settings.MultipleInstances = TaskInstancesPolicy.IgnoreNew;
            taskDefinition.Settings.DisallowStartIfOnBatteries = false;
            taskDefinition.Settings.StopIfGoingOnBatteries = false;
            taskDefinition.Settings.AllowHardTerminate = false;
            taskDefinition.Settings.StartWhenAvailable = false;
            taskDefinition.Settings.RunOnlyIfNetworkAvailable = false;
            taskDefinition.Settings.IdleSettings.StopOnIdleEnd = false;
            taskDefinition.Settings.IdleSettings.RestartOnIdle = false;

			// setting
            taskDefinition.Settings.AllowDemandStart = false;
            taskDefinition.Settings.Enabled = true;
            taskDefinition.Settings.Hidden = false;
            taskDefinition.Settings.RunOnlyIfIdle = false;
            taskDefinition.Settings.ExecutionTimeLimit = TimeSpan.Zero;
            taskDefinition.Settings.Priority = System.Diagnostics.ProcessPriorityClass.High;

            // operate
            taskDefinition.Actions.Add(new ExecAction(Application.ExecutablePath));

			// registration
            taskService.RootFolder.RegisterTaskDefinition(kTASK_SCHEDULER_NAME, taskDefinition);
        }
        else
        {
        	Microsoft.Win32.TaskScheduler.Task t = taskService.GetTask(kTASK_SCHEDULER_NAME);
            if (t != null)
            	taskService.RootFolder.DeleteTask(kTASK_SCHEDULER_NAME, true);
        }
    }
}

 

 

출처 : https://neofirst.tistory.com/346

728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - Steam Game 실행하기  (0) 2020.07.21
C# - Dispose  (0) 2018.10.12
C# - WeakReference  (0) 2018.10.11
C# - 공부하기 좋은 사이트  (0) 2018.09.05
C# - Attibute  (0) 2018.07.09
728x90

1. browser를 이용한 실행 방법

- steam:://run/appid

- 이방법을 이용하면 steam 옵션을 적용할 수 없다.

https://developer.valvesoftware.com/wiki/Steam_browser_protocol

 

Steam browser protocol - Valve Developer Community

There are numerous system-wide commands available that interact with Steam. All of them open up Steam if it is not open. They can either be typed into a command box (Start-> Run) or through your browser's address bar (you can normally create links to them

developer.valvesoftware.com

 

2. steam.exe를 이용하는 방법

- start /d "(installPath)" /b steam.exe -applaunch (appId) -nolauncher

- 이방법을 이용하는 이유는 -nolauncher와 같은 steam 옵션을 적용할 수 있다.

(installPath) == steam의 설치 경로

(appId) == steam game의 id

 

3. steam 옵션

https://developer.valvesoftware.com/wiki/Command_Line_Options

 

Command Line Options - Valve Developer Community

Source Games These command-line parameters will work with any Source engine games (Half-Life 2, Counter-strike: Source, etc.). Note:Command parameters are described inside the < and > characters. Syntax: hl2.exe Usage These options can be used with: Hammer

developer.valvesoftware.com

 

4. steam installPath 가져오는 방법

line 148 : window registry에서 steam의 설치 경로를 가져온다.

line 151 : 기본 설치 경로를 queue에 추가.

line 153 ~ 157 : 기본 설치 경로 외에 추가적인 설치 경로를 queue에 추가.

 

5. steam game의 appid 가져오는 방법

- 아래와 같이 steamapps 폴더에 .acf 파일이 있는데 이 파일이 valve에서 만든 포맷으로 안에 게임의 appid, title name 등이 전부 있다.

 

acf_parser를 이용하여 acf 파일 데이터를 읽어오면 된다.

https://stackoverflow.com/questions/39065573/reading-values-from-an-acf-manifest-file

 

Reading values from an .acf / manifest file

The file I'm trying to read is presented in the format below. How using c# can I read this file to extract property values? Are there any libraries I can leverage? "AppState" { "appid" "244210" "

stackoverflow.com

 

대충 위와같이 AcfReader를 이용하여 파일들을 parsing하고 ACFFileToStruct()를 이용하여 struct로 넣어주면 된다.

 

 

728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - 작업 스케줄러에 등록하기  (0) 2021.12.10
C# - Dispose  (0) 2018.10.12
C# - WeakReference  (0) 2018.10.11
C# - 공부하기 좋은 사이트  (0) 2018.09.05
C# - Attibute  (0) 2018.07.09
728x90

.NET Framework에서 새로운 클래스를 만들 때 여러가지 메모리 관리 디자인 패턴과 기법을 적용할 수 있지만, 한시적으로 사용해야 할 필요가 있는 자원들을 묶어서 관리할 때에는 IDisposable 패턴을 적극적으로 활용하는 것이 매우 유용합니다.

하지만 생각보다 IDisposable 패턴을 제대로 구현해서 사용하는 것은 쉽지 않으며 잘못 구현하기 쉽습니다.

이 아티클에서는 IDisposable 패턴을 구현하는 몇 가지 일반적인 전략들을 소개합니다. 잘못 설명된 부분이 있거나 보충이 필요한 부분은 댓글로 피드백을 자세히 남겨주시면 적극 반영하겠습니다.

IDisposable 인터페이스에 대한 이해

IDisposable 인터페이스가 제공하는 Dispose 메서드는 명시적이고 코드 작성자가 직접 호출할 수 있는 finalizer로, 이 메서드가 불리면 가비지 컬렉터에 의하여 나중에 호출되는 finalizer의 역할을 대체하도록 되어있습니다. 물론, 메모리 상에 할당된 메모리 블록의 해제까지 건너뛴다는 의미는 아닙니다.

그리고 무엇을 Dispose 메서드에서 제거해야 하는지 기준을 세운다면 객체에 대한 소유 권한을 정의하는 것이 필요합니다. 적어도 다음의 경우에는 확실히 Dispose 메서드 내에서 정리가 되어야 합니다.

  • 해당 객체를 Dispose 하게 되면 외부에서 더 이상 사용하는 것이 의미가 없는 객체 (예를 들어 클래스 내부에서 사용하던 파일 입출력 관련 객체, 비동기 작업을 위하여 만들어 놓은 스레드 관련 객체)
  • 외부에서 전달받은 객체이지만 객체의 생명 주기를 위탁하여 관리하도록 지정한 객체 (예를 들어 StreamReader나 StreamWriter가 객체 생성 시 인자로 Stream을 받는 사례)

가장 기본이 되는 IDisposable 구현 패턴

.NET 은 finalizer에 해당되는 멤버를 재정의할 수 있습니다. 하지만 finalizer가 언제 호출이 될 것인지 기약할 수 없으므로 이 finalizer를 대신하여 좀 더 이른 시기에 명시적으로 소멸자와 동등한 효과를 낼 수 있도록 만든 것이 바로 IDisposable.Dispose 메서드가 되겠습니다.

IDisposable 패턴을 처음 구현할 때에는 다음의 사항들이 핵심이 됩니다.

  • protected virtual void Dispose(bool disposing) 메서드를 추가합니다. sealed 클래스에 대해서는 private void Dispose(bool disposing)으로 바꾸어 정의합니다.
  • 객체가 dispose 처리가 이루어진 상태인지를 관리할 수 있는 boolean 필드를 하나 추가하고 이 필드는 기본값을 false로 설정합니다.
  • Dispose(bool disposing) 호출 시 다음의 로직을 구현합니다.
  • 만약 객체가 dispose 상태인 경우에는 함수를 종료합니다.
  • 객체가 dispose 상태임을 필드에 지정합니다. (true로 설정)
  • disposing 매개 변수의 상태와 관계없이 P/Invoke 등을 활용하여 메모리 할당을 받은 나머지 모든 리소스들에 대해 할당을 해제하는 코드를 추가합니다.
  • disposing 매개 변수가 true로 전달되는 경우는 명시적으로 Dispose 메서드를 호출한 경우이며, 이 때에 다른 모든 IDisposable을 구현하는 객체들을 Dispose 처리합니다.
  • IDisposable.Dispose 메서드 구현 시 Dispose(true)를 호출하고 finalizer 호출을 건너뛰기 위하여 GC.SuppressFinalize(this)를 호출합니다.
  • 소멸자에서는 Dispose(false)를 호출합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
public DisposableSample()
{ }
    ~DisposableSample()
{
this.Dispose(false);
}

private bool disposed;
    public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
    protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing)
{
// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
}
// .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
this.disposed = true;
}
}

IDisposable 객체의 컬렉션에 대한 소거

소켓, 스레드 풀, 혹은 커넥션 풀 같은 컬렉션을 객체 내부에 보관해야 하는 경우도 있습니다. 이러한 경우에도 컬렉션 내의 모든 IDisposable 객체에 대해서 정리를 하는 것이 필요합니다.

  • 컬 렉션 내의 모든 요소가 IDisposable 인터페이스를 구현하고 있다면 Dispose(bool disposing) 메서드에서 disposing이 true일 때 정리를 하면 됩니다. 그렇지 않은 경우, disposing 매개 변수의 상태에 무관하게 정리합니다.
  • Dispose 작업 도중 새로운 항목이 추가되는 것을 방지하기 위하여 disposed 필드를 확인하도록 하는 코드를 추가해야 할 수 있습니다.
  • 컬렉션 내의 모든 요소를 배열에 복사하여 Immutable Collection으로 변환한 다음 하나씩 방문하여 Dispose를 진행하고, 최종적으로 컬렉션의 요소들을 모두 제거합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
public DisposableSample()
{
this.items = new List<IDisposable>();
}
    ~DisposableSample()
{
this.Dispose(false);
}

private bool disposed;
private List<IDipsosable> items;
    public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
    protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing)
{
// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
IDisposable[] targetList = new IDisposable[this.items.Count];
this.items.CopyTo(targetList);
foreach (IDisposable eachItem in targetList)
{
eachItem.Dispose();
}
this.items.Clear();
}
// .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
this.disposed = true;
}
}

Dispose 메서드 내의 예외 처리

만약 Dispose 메서드를 실행하는 도중에 예외가 발생한다면, CA1065의 지침에 따라 명시적인 Dispose 호출이었든 아니었든 예외를 전파하지 않도록 처리하는 것이 필요합니다. 다만 명시적으로 Dispose를 호출하면서 예외가 발생했다면 예외를 전파하지 않는 대신 적절한 예외 처리는 필요합니다.

이 부분에 대한 자세한 내용은 https://msdn.microsoft.com/ko-kr/library/bb386039.aspx 페이지의 내용을 참고하시면 도움이 될 것입니다.

컬렉션 내의 모든 요소들을 Dispose 하는 코드를 조금 더 보강하면 다음과 같이 고쳐쓸 수 있겠습니다.

public class DisposableSample : IDisposable
{
public DisposableSample()
{
this.items = new List<IDisposable>();
}
    ~DisposableSample()
{
this.Dispose(false);
}
    private bool disposed;
private List<IDisposable> items;
    public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
    protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
try
{
if (disposing)
{
// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
IDisposable[] targetList = new IDisposable[this.items.Count];
this.items.CopyTo(targetList);
foreach (IDisposable eachItem in targetList)
{
try { eachItem.Dispose(); }
catch (Exception ex) { /* 예외 처리를 수행합니다. */ }
finally { /* 정리 작업을 수행합니다. */ }
}
this.items.Clear();
}
try { /* .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다. */ }
catch { /* 예외 처리를 수행합니다. */ }
finally
{
/* 정리 작업을 수행합니다. */
this.disposed = true;
}
}
finally { /* 정리 작업을 수행합니다. */ }
}
}

소유하고 있는 객체들에 대한 Dispose 또는 정리 작업들을 각각 try, catch, finally 블록안에 두어 예외가 발생하면 적절한 예외 처리를 할 수 있게 하고, Dispose 메서드 전체에 대해서 try, finally 블록 안에 두어 예외가 전파되지 않도록 하였습니다.

결론

.NET Framework 기반의 응용프로그램이 안정적으로 장시간 실행될 수 있게 만들어야 할 때 고려해야 할 요소들 가운데에서 가장 비중있게 다루어야 할 부분이 바로 메모리 관리입니다. IDisposable 인터페이스를 통한 명시적인 finalizer 호출은 적절하게 활용하면 응용프로그램의 메모리 관리를 단순하게 만드는데 큰 도움을 줍니다.


출처 : https://medium.com/rkttu/idisposable-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-4fa0fcf0e67a

728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - 작업 스케줄러에 등록하기  (0) 2021.12.10
C# - Steam Game 실행하기  (0) 2020.07.21
C# - WeakReference  (0) 2018.10.11
C# - 공부하기 좋은 사이트  (0) 2018.09.05
C# - Attibute  (0) 2018.07.09
728x90

WeakReference Class를 사용하여 메모리를 관리해보자. 


C#의 가장 큰 특징중 하나는 가비지 컬렉터(Garbage Collector)가 자동으로 메모리를 관리해준다는 것입니다.

상식적으로 가비지 컬렉터가 작동된다면 사용하지 않은 메모리는 자동으로 반환해야되지만

실제로 메모리가 반환되지 않고 계속 늘어나기도 합니다.


계속 늘어나는 메모리를 해제하고 싶지만 C#에는 따로 메모리를 해제하는 명령어가 없어서 메모리 릭(Memory leak)이 발생하기도 합니다.


이렇게 메모리가 계속 늘어나는 이유가 뭘까요?


가비지(Garbage)는 더 이상 참조가 없는 메모리를 뜻합니다.

메모리가 반환되지 않고 계속 늘어나고 있다면 어디선가 반환되야될 메모리를 의도치 않게 참조 하고 있어서 

가바지 컬렉터가 반환메모리로 인식하지 않아 청소를 못하고 자꾸 쌓이고만 있는것입니다.


저 또한 겪었던 문제로


하나의 UserControl을 만들었습니다.

이 UserControl은 DB에서 Select문으로 데이터를 끌어올때마다 생성하여 UserControl내부 로직을 타고 화면에 뿌려줍니다.

그 후 사용이 끝난 UserControl은 Dispose()를 해주지만 이상하게 검색을 할때마다 메모리가 늘어납니다.

결국 메모리 릭(Memory leak)이 발생하더군요.

저는 분명 모든 리소스를 해제했다고 생각하고 있었지만 어디선가 참조가 되고 있었는것이죠.

(결국 문제점을 발견하지 못했습니다. ㅠㅠ Garbage 강제 콜을 해도 메모리가 2%늘었다 1%줄었다 반복하더니 메모리 릭이 발생하더군요.)


이럴때 WeakReference Class를 사용해주면 됬었지만! 

그때 당시 저는 이 방법을 몰라서 한참을 돌아돌아 프로그램을 완성했었습니다.

(어찌했는지 기억도 잘 안나네요 ㅠㅠ)


WeakReference는 가바지 수집에 의한 개체 회수를 허용하면서 개체를 참조하는 일명 약한 참조를 만들어 냅니다.


아래 예제 소스를 보면서 설명하겠습니다.

(예제소스를 실행해보고 싶으시면 라벨 두개와 버튼 하나를 만들어서 복사후 연결만 시켜주면됩니다.)



<p>
using System;
using System.Windows.Forms;
 
namespace WeakReferenceTest
{
    public partial class WeakReference_Form : Form
    {
        stock phone;
        stock notebook;
        stock stock1;
        WeakReference stock2;
 
        public WeakReference_Form()
        {
            InitializeComponent();
            //Stock라는 클래스를 만들어 각각 Phone, NoteBook라는 물품을 담게 하였습니다.
            phone = new stock("Phone");
            notebook = new stock("NoteBook");
             
            //각각 참조를 거는데 phone은 일반적으로 사용하는 강한참조
            //notebook은 WeakReference를 이용한 약한 참조를 걸어보도록 하겠습니다.
            stock1 = phone;
            stock2 = new WeakReference(notebook);
 
            //각각 라벨에 출력해보겠습니다.
            labelControl1.Text = stock1 == null ? "null" : stock1.name;
            labelControl2.Text = stock2.Target == null ? "null" : (stock2.Target as stock).name;
 
            //당연한 이야기겠지만
            //------------  label1 = Phone
            //------------  label2 = NoteBook
            //가 출력됩니다.
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            //버튼 클릭시 phone과 notebook에 null을 넣어 초기화 시켜줍니다.
            phone = null;
            notebook = null;
 
            //가바지 컬렉터를 강제로 작동시켜보겠습니다.
            System.GC.Collect(0, GCCollectionMode.Forced);
            System.GC.WaitForFullGCComplete();
 
            //다시한번 참조한 값을 각각 라벨에 출력해보겠습니다.
            labelControl1.Text = stock1 == null ? "null" : stock1.name;
            labelControl2.Text = stock2.Target == null ? "null" : (stock2.Target as stock).name;
 
            //어떻게 나올지 예상하셨나요?
            //필자는 stock1과 stock2 모두 참조 객체가 null로 변경이 되었으니 null이 출력된다고 생각했습니다.
            //하지만 결과는
            //----------  label1 = Phone
            //----------  label2 = null
            //과 같이 출력됩니다.
            //강한참조는 참조 객체를 초기화 시켜도 Data를 붙잡고 있어서 메모리 회수가 안되는 것입니다.
        }
    }
 
    public class stock
    {
        public string name = "";
        public stock(string name)
        {
            this.name = name;
        }
    }
}
</p>


C#은 메모리를 컨트롤 안해도 된다는 생각을 가지고 있었지만 (필자가 배울때는 그렇게 배워서 ㅠㅠ)

실제로는 C나 C++만큼은 아니지만 메모리 관리를 해야된다고 생각합니다.


출처 : http://mirwebma.tistory.com/142

728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - 작업 스케줄러에 등록하기  (0) 2021.12.10
C# - Steam Game 실행하기  (0) 2020.07.21
C# - Dispose  (0) 2018.10.12
C# - 공부하기 좋은 사이트  (0) 2018.09.05
C# - Attibute  (0) 2018.07.09
728x90

http://www.csharpstudy.com/

728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - 작업 스케줄러에 등록하기  (0) 2021.12.10
C# - Steam Game 실행하기  (0) 2020.07.21
C# - Dispose  (0) 2018.10.12
C# - WeakReference  (0) 2018.10.11
C# - Attibute  (0) 2018.07.09
728x90

애트리뷰트는 클래스에 메타데이터를 추가할수 있도록 제공한다.

주석과는 달리 클래스부터 시작해서 메소드구조체생성자프로퍼티필드이벤트인터페이스 등 여러가지 요소에 애트리뷰트를 사용할 수 있다.

 

우리가 필요에 의해서 이 애트리뷰트를 사용해 코드 앞에다 설명을 덧붙일 수도 있다.

애트리뷰트의 기본 형식은 다음과 같다:

1 [attribute(positional_parameter, name_parameter = value, ...)]

여기서 positional_parameter는 위치지정 파라미터라고 해서 반드시 적어야하는 부분으로, " "를 사용하여 작성한다그리고 name_parameter는 명명 파라미터로선택적인 정보이며 = 를 사용해서 값을 기입한다.

이 애트리뷰트는 크게 두가지로 나뉘는데사용자가 정의하는 커스텀 애트리뷰트와 내장되어 있는 공통 애트리뷰트로 나뉜다공통 애트리뷰트의 경우는 추가된 정보가 컴파일 방식에 영향을 줄 수 있는데반대로 커스텀 애트리뷰트는 영향을 주지 못한다.

 

대표적인 공통 애트리뷰트로는

Obsolete, Conditional, DllImport이 있다.


1. Obsolete

함수위에 [Obsolete(“출력하고싶은내용”] 을 걸면,

해당 함수 실행시 경고창이 콘솔에 출력된다.


2. Conditional

함수위에 [Conditional(“조건”] 을 걸고,

코드상에 #define 조건을 선언하면해당 함수가 실행되고해당 정의를 빼버리면 실행되지 않는다.


3.  DllImport

함수위에 [DllImport(“Example.dll”)] 을 걸고 extern 키워드를 이용하여 외부 dll에 정의 되어있는 함수를 사용할수 있다.

Public static extern int MessageBox()

 

이 외에도 많은 종류의 애트리뷰트를 제공한다.

 

유니티에서의 제공하는 커스텀 애트리뷰트로는 다음과 같은 것이 있다:


1) AddComponentMenu

기본적으로 스크립트는 유니티의 Component->Scripts 메뉴에 자동추가된다자동추가말고 아무데나 맘대로 넣고 싶으면 AddComponentMenu를 사용한다


2)  ContextMenu

스크립트를 우클릭시 뜨는 context menu에 커맨드를 추가할 수 있다


3) ExecuteInEditMode 기본적으로 play mode일 때만 스크립트가 실행되나 attribute를 사용하면edit mode일 때도 스크립트가 실행되게 한다.

 

4) HideInInspector

inspector에서 안보이게 한다.

 

5) NonSerialized

앞서 HideInInspector는 값을 유지하지만이건 그냥 리셋하여 디폴트값 으로 바꾼다


6) RPC

원격 컴퓨터의 해당 함수를 호출하는 것을 가능케 하는 애트리뷰트다. Remote Procedure Calls라고도 부르며,  유니티 5.1부터는 사용하지 않는 애트리뷰트이다.

 

7) RequireComponent 

함께 필요한 다른 컴포넌트가 자동으로 추가된다.

 

8) Serializable

직렬화 하여 유니티에서 해당 클래스의 내용물이 Inspector에서 보일 수 있도록 한다.


9) SerializeField

private필드를 강제로 serialize한다그러므로 inspector에서도 편집이 가능해진다.




출처 : https://blog.naver.com/captainj/221102947400


728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - 작업 스케줄러에 등록하기  (0) 2021.12.10
C# - Steam Game 실행하기  (0) 2020.07.21
C# - Dispose  (0) 2018.10.12
C# - WeakReference  (0) 2018.10.11
C# - 공부하기 좋은 사이트  (0) 2018.09.05

+ Recent posts