일반적인 C# 코드 규칙
코딩 규칙은 개발팀 내에서 코드 가독성, 일관성 및 협업을 유지하는 데 필수적입니다. 업계 사례와 설정된 지침을 따르는 코드는 이해하기 쉽고, 유지 관리하고 확장하기도 편합니다. 대부분의 프로젝트는 코드 규칙을 통해 일관된 스타일을 적용합니다. dotnet/docs
프로젝트와 dotnet/samples
프로젝트도 예외는 아닙니다. 이 문서 시리즈에서는 코딩 규칙과 이를 적용하는 데 사용하는 도구를 알아봅니다. 규칙을 있는 그대로 적용해도 되고, 팀의 요구에 맞게 수정할 수도 있습니다.
Microsoft는 다음 목표를 기준으로 규칙을 선택했습니다.
- 정확성: Microsoft 샘플은 사용자의 애플리케이션으로 복사 및 붙여넣기 하여 사용됩니다. 따라서 Microsoft는 여러 번의 편집 후에도 복원력과 정확성을 유지하는 코드를 만들 필요가 있습니다.
- 교육 효과: 샘플의 목적은 .NET 및 C#를 모두 가르치는 것입니다. 따라서 어떠한 언어 기능이나 API에도 제한을 두지 않습니다. 대신, 이러한 샘플은 기능 선택이 제대로 된 경우에 교육 효과가 있습니다.
- 일관성: 독자는 콘텐츠 전체에서 일관된 환경을 기대합니다. 모든 샘플은 동일한 스타일을 따라야 합니다.
- 채택성: Microsoft는 새 언어 기능을 사용하도록 샘플을 적극적으로 업데이트합니다. 이러한 방식은 새로운 기능이 널리 알려지고 모든 C# 개발자에게 더 친숙해지도록 만듭니다.
Important
이러한 지침은 Microsoft에서 샘플 및 설명서를 개발하는 데 사용됩니다. 지침은 .NET 런타임, C# 코딩 스타일 및 C# 컴파일러(roslyn) 지침에서 채택되었습니다. 이러한 지침은 오랜 기간 오픈 소스 개발에서 테스트를 거쳐 선택된 것입니다. 지침은 커뮤니티 구성원이 런타임 및 컴파일러 프로젝트에 참여하는 데 도움을 주었습니다. 지침의 쓰임새는 일반적인 C# 규칙의 예이지, 신뢰할 수 있는 목록(이 경우 프레임워크 디자인 지침 참조)은 아닙니다.
문서 코딩 규칙은 교육 효과 및 채택성 목표로 인해 런타임 및 컴파일러 규칙과 차별화됩니다. 런타임과 컴파일러에는 실행 부하 과다 경로에 대한 엄격한 성능 메트릭이 있습니다. 다른 많은 애플리케이션은 그렇지 않습니다. Microsoft의 교육 효과 목표는 어떤 구문도 금지하지 않을 것을 의무화합니다. 대신, 구문 사용이 필요한 경우를 샘플로 보여줍니다. Microsoft는 대부분의 프로덕션 애플리케이션보다 더 적극적으로 샘플을 업데이트합니다. 채택성 목표에 따라 Microsoft는 사용자가 오늘 작성해야 하는 코드를 보여주며, 작년에 작성된 코드에 변경이 필요 없는 경우도 예외가 아닙니다.
이 문서는 Microsoft 지침을 설명합니다. 지침은 시간이 지남에 따라 진화했으며, 지침을 따르지 않는 샘플도 보일 수 있습니다. 그러한 샘플을 규정 준수로 인도하는 PR이나, 업데이트가 필요한 샘플로 주의를 끄는 문제 제출을 환영합니다. Microsoft 지침은 오픈 소스이며, PR과 문제 제출을 환영합니다. 하지만 제출로 권장 사항을 변경하려는 경우 먼저 논의를 위해 문제를 오픈하세요. 지침을 사용하실 수 있으며, 요구 사항에 맞게 조정할 수 있습니다.
도구 및 분석기
팀에서 규칙을 적용하는 데 도구를 사용할 수 있습니다. 코드 분석을 사용하여 원하는 규칙을 적용할 수 있습니다. 또한 editorconfig를 만들어 Visual Studio에서 사용자의 스타일 지침을 자동으로 적용하게 할 수 있습니다. 우선 시작점에서 dotnet/docs 리포지토리 파일을 복사하여 Microsoft 스타일을 사용할 수 있습니다.
이러한 도구를 사용하면 선호하는 지침을 팀에서 더 쉽게 채택할 수 있습니다. Visual Studio는 범위 내 모든 .editorconfig
파일에 규칙을 적용하여 코드의 형식을 지정합니다. 여러 구성을 사용하여 전사적 규칙, 팀 규칙 및 세부적인 프로젝트 규칙을 적용할 수 있습니다.
코드 분석은 활성화된 규칙이 위반될 때 경고 및 진단을 생성합니다. 프로젝트에 적용하려는 규칙을 구성합니다. 그런 다음 각 CI 빌드는 개발자가 규칙을 위반할 때 개발자에게 알립니다.
진단 ID
- 자체 분석기를 빌드할 때 적절한 진단 ID 선택
언어 지침
다음 섹션에서는 .NET 문서 팀이 코드 예제와 샘플을 준비할 때 따르는 방식을 설명합니다. 일반적으로 다음 방식을 따릅니다.
- 가능하면 최신 버전의 언어 기능과 C#을 활용합니다.
- 사용되지 않거나 오래된 언어 구문은 사용하지 않습니다.
- 적절히 처리할 수 있는 예외만 catch하며, 제네릭 예외는 catch하지 않습니다.
- 특정 예외 유형을 사용하여 의미 있는 오류 메시지를 제공합니다.
- 컬렉션 조작에 LINQ 쿼리와 메서드를 사용하여 코드 가독성을 높입니다.
- I/O 바인딩된 작업에 async 및 await를 사용한 비동기 프로그래밍을 사용합니다.
- 교착 상태에 주의하고 적절한 경우 Task.ConfigureAwait을(를) 사용합니다.
- 데이터 형식에 런타임 형식 대신 언어 키워드를 사용합니다. 예를 들어 System.String 대신
string
을(를), System.Int32 대신int
을(를) 사용합니다. - 부호 없는 형식 대신
int
을(를) 사용합니다. C# 전체에서int
사용은 일반적이며,int
을(를) 사용할 때 다른 라이브러리와 더 쉽게 상호 작용합니다. 부호 없는 데이터 형식과 관련된 문서의 경우는 예외입니다. - 독자가 식에서 형식을 유추할 수 있는 경우에만
var
을(를) 사용합니다. 독자는 문서 플랫폼에서 샘플을 봅니다. 변수 형식을 표시하는 호버 또는 도구 팁이 없습니다. - 명확성과 단순성을 염두에 두고 코드를 작성합니다.
- 지나치게 복잡하고 복잡한 코드 논리는 피합니다.
다음은 좀 더 구체적인 지침입니다.
문자열 데이터
다음 코드에 나와 있는 것처럼 문자열 보간을 사용하여 짧은 문자열을 연결합니다.
string displayName = $"{nameList[n].LastName}, {nameList[n].FirstName}";
특히 많은 양의 텍스트를 사용할 때 문자열을 루프에 추가하려면 System.Text.StringBuilder 개체를 사용합니다.
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala"; var manyPhrases = new StringBuilder(); for (var i = 0; i < 10000; i++) { manyPhrases.Append(phrase); } //Console.WriteLine("tra" + manyPhrases);
배열
- 선언 줄에서 배열을 초기화할 때는 간결한 구문을 사용합니다. 다음 예제에서는
string[]
대신var
을(를) 사용할 수 없습니다.
string[] vowels1 = { "a", "e", "i", "o", "u" };
- 명시적 인스턴스화를 사용하는 경우
var
을 사용할 수 있습니다.
var vowels2 = new string[] { "a", "e", "i", "o", "u" };
대리자
- 대리자 형식을 정의하는 대신
Func<>
및Action<>
을 사용합니다. 클래스에서 대리자 메서드를 정의합니다.
Action<string> actionExample1 = x => Console.WriteLine($"x is: {x}");
Action<string, string> actionExample2 = (x, y) =>
Console.WriteLine($"x is: {x}, y is {y}");
Func<string, int> funcExample1 = x => Convert.ToInt32(x);
Func<int, int, int> funcExample2 = (x, y) => x + y;
Func<>
또는Action<>
대리자로 정의된 시그니처를 사용하여 메서드를 호출합니다.
actionExample1("string for x");
actionExample2("string for x", "string for y");
Console.WriteLine($"The value is {funcExample1("1")}");
Console.WriteLine($"The sum is {funcExample2(1, 2)}");
대리자 형식의 인스턴스를 만드는 경우 간결한 구문을 사용합니다. 클래스에서 일치하는 시그니처가 있는 대리자 형식 및 메서드를 정의합니다.
public delegate void Del(string message); public static void DelMethod(string str) { Console.WriteLine("DelMethod argument: {0}", str); }
대리자 형식의 인스턴스를 만들고 호출합니다. 다음 선언에서는 압축된 구문을 보여 줍니다.
Del exampleDel2 = DelMethod; exampleDel2("Hey");
다음 선언에서는 전체 구문을 사용합니다.
Del exampleDel1 = new Del(DelMethod); exampleDel1("Hey");
예외 처리의 try-catch
및 using
문
대부분의 예외 처리에서는 try-catch 문을 사용합니다.
static double ComputeDistance(double x1, double y1, double x2, double y2) { try { return Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } catch (System.ArithmeticException ex) { Console.WriteLine($"Arithmetic overflow or underflow: {ex}"); throw; } }
C# using 문을 사용하면 코드를 간소화할 수 있습니다.
finally
블록의 코드가 Dispose 메서드 호출뿐인 try-finally 문이 있는 경우에는using
문을 대신 사용합니다.다음 예제에서
try-finally
문은finally
블록의Dispose
만 호출합니다.Font bodyStyle = new Font("Arial", 10.0f); try { byte charset = bodyStyle.GdiCharSet; } finally { if (bodyStyle != null) { ((IDisposable)bodyStyle).Dispose(); } }
using
문을 사용하여 같은 작업을 수행할 수 있습니다.using (Font arial = new Font("Arial", 10.0f)) { byte charset2 = arial.GdiCharSet; }
중괄호가 필요하지 않은 새
using
구문을 사용합니다.using Font normalStyle = new Font("Arial", 10.0f); byte charset3 = normalStyle.GdiCharSet;
&&
및 ||
연산자
다음 예제와 같이, 비교를 수행할 때
&
대신&&
을(를),||
대신|
을(를) 사용합니다.Console.Write("Enter a dividend: "); int dividend = Convert.ToInt32(Console.ReadLine()); Console.Write("Enter a divisor: "); int divisor = Convert.ToInt32(Console.ReadLine()); if ((divisor != 0) && (dividend / divisor) is var result) { Console.WriteLine("Quotient: {0}", result); } else { Console.WriteLine("Attempted division by 0 ends up here."); }
제수가 0인 경우 if
문의 두 번째 절을 실행하면 런타임 오류가 발생합니다. 그러나 첫 번째 식이 false이면 && 연산자는 단락(short-circuit)됩니다. 즉, 두 번째 식을 계산하지 않습니다. divisor
가 0인 경우 & 연산자는 둘 다를 계산하므로 런타임 오류가 발생합니다.
new
연산자
다음 선언에 나와 있는 것처럼 간결한 형식의 개체 인스턴스화 중 하나를 사용합니다.
var firstExample = new ExampleClass();
ExampleClass instance2 = new();
앞의 선언은 다음 선언과 같습니다.
ExampleClass secondExample = new ExampleClass();
다음 예제에 나와 있는 것처럼 개체 이니셜라이저를 사용하여 개체 만들기를 간소화합니다.
var thirdExample = new ExampleClass { Name = "Desktop", ID = 37414, Location = "Redmond", Age = 2.3 };
다음 예제에서는 앞의 예제와 같은 속성을 설정하지만, 이니셜라이저를 사용하지는 않습니다.
var fourthExample = new ExampleClass(); fourthExample.Name = "Desktop"; fourthExample.ID = 37414; fourthExample.Location = "Redmond"; fourthExample.Age = 2.3;
이벤트 처리
- 나중에 제거할 필요가 없는 이벤트 처리기를 정의하려는 경우 람다 식을 사용합니다.
public Form2()
{
this.Click += (s, e) =>
{
MessageBox.Show(
((MouseEventArgs)e).Location.ToString());
};
}
람다 식은 다음과 같은 기존 정의를 줄여 줍니다.
public Form1()
{
this.Click += new EventHandler(Form1_Click);
}
void Form1_Click(object? sender, EventArgs e)
{
MessageBox.Show(((MouseEventArgs)e).Location.ToString());
}
정적 멤버
ClassName.StaticMember와 같이 클래스 이름을 사용하여 static 멤버를 호출합니다. 이렇게 하면 정적 액세스가 명확하게 표시되므로 코드를 보다 쉽게 읽을 수 있습니다. 파생 클래스 이름을 사용하여 기본 클래스에 정의된 정적 멤버를 정규화하지 않습니다. 이 코드는 컴파일되기는 하지만 가독성이 떨어지며 나중에 파생 클래스와 이름이 같은 정적 멤버를 추가하면 코드가 손상될 수도 있습니다.
LINQ 쿼리
쿼리 변수에 의미 있는 이름을 사용합니다. 다음 예제에서는 Seattle 거주 고객에 대해
seattleCustomers
를 사용합니다.var seattleCustomers = from customer in customers where customer.City == "Seattle" select customer.Name;
별칭을 사용하여 익명 형식의 속성 이름 대/소문자를 올바르게 표시합니다(파스칼식 대/소문자 사용).
var localDistributors = from customer in customers join distributor in distributors on customer.City equals distributor.City select new { Customer = customer, Distributor = distributor };
결과의 속성 이름이 모호하면 속성 이름을 바꿉니다. 예를 들어 쿼리에서 고객 이름과 배포자 ID를 반환하는 경우 결과에서 이러한 정보를
Name
및ID
로 유지하는 대신Name
은 고객의 이름이고ID
는 배포자의 ID임을 명확하게 나타내도록 이름을 바꿉니다.var localDistributors2 = from customer in customers join distributor in distributors on customer.City equals distributor.City select new { CustomerName = customer.Name, DistributorID = distributor.ID };
쿼리 변수 및 범위 변수의 선언에서 암시적 형식을 사용합니다. LINQ 쿼리의 암시적 형식에 대한 이 지침은 암시적 형식 지역 변수에 대한 일반 규칙을 재정의합니다. LINQ 쿼리는 무명 형식을 만드는 프로젝션을 사용하는 경우가 많습니다. 다른 쿼리 식은 중첩된 제네릭 형식으로 결과를 만듭니다. 암시적 형식 변수는 더 읽기 쉬운 경우가 많습니다.
var seattleCustomers = from customer in customers where customer.City == "Seattle" select customer.Name;
위 예제처럼
from
절 아래의 쿼리 절을 정렬합니다.where
절을 다른 쿼리 절 앞에 사용하여, 뒤에 있는 쿼리 절이 필터링으로 범위가 좁아진 데이터 집합에 대해 작동하게 합니다.var seattleCustomers2 = from customer in customers where customer.City == "Seattle" orderby customer.Name select customer;
하나의
join
절 대신 여러 개의from
절을 사용하여 내부 컬렉션에 액세스합니다. 예를 들어Student
개체 컬렉션이 각각 테스트 점수 컬렉션을 포함하는 경우 다음 쿼리를 실행하면 90점보다 높은 각 점수와 해당 점수를 받은 학생의 성이 반환됩니다.var scoreQuery = from student in students from score in student.Scores! where score > 90 select new { Last = student.LastName, score };
암시적 형식 지역 변수
할당 오른쪽에서 변수 형식이 명확하면 지역 변수에 대해 암시적 형식을 사용합니다.
var message = "This is clearly a string."; var currentTemperature = 27;
할당 오른쪽에서 변수 형식이 명확하지 않으면 var를 사용하지 않습니다. 메서드 이름에서 형식이 명확하다고 가정하지 않습니다. 변수 형식이
new
연산자, 명시적 캐스트 또는 리터럴 값에 대한 할당인 경우 명확한 것으로 간주합니다.int numberOfIterations = Convert.ToInt32(Console.ReadLine()); int currentMaximum = ExampleClass.ResultSoFar();
변수 이름을 사용하여 변수의 형식을 지정하지 않습니다. 이렇게 하면 형식이 올바르게 지정되지 않을 수 있습니다. 대신 형식을 사용하여 형식을 지정하고, 변수 이름을 사용하여 변수의 의미 체계 정보를 나타냅니다. 다음 예제에서는 형식에
string
을(를) 사용하고, 콘솔에서 읽은 정보의 의미를 나타내는 데iterations
과(와) 같은 것을 사용해야 합니다.var inputInt = Console.ReadLine(); Console.WriteLine(inputInt);
dynamic 대신
var
를 사용하지 않습니다. 런타임 형식 유추를 원하는 경우dynamic
을 사용합니다. 자세한 내용은 dynamic 형식 사용(C# 프로그래밍 가이드)을 참조하세요.for
루프의 루프 변수에 암시적 형식을 사용합니다.다음 예제에서는
for
문에서 암시적 형식을 사용합니다.var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala"; var manyPhrases = new StringBuilder(); for (var i = 0; i < 10000; i++) { manyPhrases.Append(phrase); } //Console.WriteLine("tra" + manyPhrases);
foreach
루프의 루프 변수 형식을 결정하는 데 암시적 형식을 사용하지 않습니다. 대부분의 경우 컬렉션 요소의 형식이 즉시 명확하지는 않습니다. 해당 요소의 형식을 유추하는 데 컬렉션의 이름에만 의존해서는 안 됩니다.다음 예제에서는
foreach
문에서 명시적 형식을 사용합니다.foreach (char ch in laugh) { if (ch == 'h') { Console.Write("H"); } else { Console.Write(ch); } } Console.WriteLine();
LINQ 쿼리의 결과 시퀀스에 암시적 형식을 사용합니다. LINQ 섹션에서는 많은 LINQ 쿼리로 인해 암시적 형식을 사용해야 하는 무명 형식이 발생하는 것을 설명합니다. 다른 쿼리는 더 읽기 쉬운 중첩된 제네릭 형식
var
을(를) 생성합니다.참고 항목
반복 가능한 컬렉션의 요소 형식을 실수로 변경하지 않도록 주의해야 합니다. 예를 들어
foreach
문에서 System.Linq.IQueryable을 System.Collections.IEnumerable으로 전환하기 쉬운데 그러면 쿼리 실행이 변경됩니다.
일부 샘플에서는 식의 자연스러운 형식을 설명합니다. 이러한 샘플에서는 컴파일러가 자연 형식을 선택할 수 있도록 var
을(를) 사용해야 합니다. 이러한 예제는 덜 명확하지만 샘플에 var
을(를) 사용해야 합니다. 텍스트는 동작을 설명해야 합니다.
using 지시문을 네임스페이스 선언 외부에 배치
using
지시문이 네임스페이스 선언 외부에 있는 경우 가져온 네임스페이스는 정규화된 이름입니다. 정규화된 이름은 더 명확합니다. using
지시문이 네임스페이스 내부에 있는 경우 해당 네임스페이스에 상대적이거나 정규화된 이름일 수 있습니다.
using Azure;
namespace CoolStuff.AwesomeFeature
{
public class Awesome
{
public void Stuff()
{
WaitUntil wait = WaitUntil.Completed;
// ...
}
}
}
WaitUntil 클래스에 대한 참조(직접 또는 간접)가 있다고 가정합니다.
이제 약간 변경해 보겠습니다.
namespace CoolStuff.AwesomeFeature
{
using Azure;
public class Awesome
{
public void Stuff()
{
WaitUntil wait = WaitUntil.Completed;
// ...
}
}
}
오늘 컴파일합니다. 또 내일 컴파일합니다. 하지만 다음 주에는 이전(손대지 않은) 코드가 두 가지 오류로 실패합니다.
- error CS0246: The type or namespace name 'WaitUntil' could not be found (are you missing a using directive or an assembly reference?)
- error CS0103: The name 'WaitUntil' does not exist in the current context
종속성 중 하나가 네임스페이스에 이 클래스를 사용한 후 .Azure
(으)로 끝났습니다.
namespace CoolStuff.Azure
{
public class SecretsManagement
{
public string FetchFromKeyVault(string vaultId, string secretId) { return null; }
}
}
네임스페이스 내부에 배치되는 using
지시문은 상황에 따라 다르며 이름 확인을 복잡하게 만듭니다. 이 예제에서는 첫 번째 네임스페이스를 찾습니다.
CoolStuff.AwesomeFeature.Azure
CoolStuff.Azure
Azure
CoolStuff.Azure
또는 CoolStuff.AwesomeFeature.Azure
과(와) 일치하는 새 네임스페이스를 추가하면 전역 Azure
네임스페이스보다 먼저 일치합니다. using
선언에 global::
한정자를 추가하여 이를 해결할 수 있습니다. 그러나 네임스페이스 외부에 using
선언을 배치하는 것이 더 쉽습니다.
namespace CoolStuff.AwesomeFeature
{
using global::Azure;
public class Awesome
{
public void Stuff()
{
WaitUntil wait = WaitUntil.Completed;
// ...
}
}
}
스타일 지침
일반적으로 코드 샘플에는 다음 형식을 사용합니다.
- 들여쓰기에 네 개의 공백을 사용합니다. 탭을 사용하지 않습니다.
- 가독성을 위해 코드를 일관되게 정렬합니다.
- 특히 모바일 화면에서 문서 코드의 가독성을 위해 줄을 65자로 제한합니다.
- 긴 문을 여러 줄로 나누면 명확성이 향상됩니다.
- 중괄호에는 "Allman" 스타일을 사용합니다. 여는 중괄호와 닫는 중괄호는 그 자체가 새 줄입니다. 중괄호는 현재 들여쓰기 수준에 맞춰 정렬됩니다.
- 필요한 경우 이진 연산자 앞에 줄 바꿈이 있어야 합니다.
주석 스타일
한 줄 주석(
//
)으로 간단하게 설명합니다.여러 줄 주석(
/* */
)으로 길게 설명하지 않습니다.
코드 샘플의 주석은 지역화되지 않았습니다. 즉, 코드에 포함된 설명은 번역되지 않습니다. 더 긴 설명 텍스트를 지역화할 수 있도록 도우미 문서에 배치해야 합니다.메서드, 클래스, 필드 및 모든 공용 멤버는 XML 주석을 사용하여 설명합니다.
코드 줄의 끝이 아닌 별도의 줄에 주석을 배치합니다.
주석 텍스트는 대문자로 시작합니다.
주석 텍스트 끝에는 마침표를 붙입니다.
다음 예제와 같이 주석 구분 기호(
//
)와 주석 텍스트 사이에 공백을 하나 삽입합니다.// The following declaration creates a query. It does not run // the query.
레이아웃 규칙
효율적인 레이아웃에서는 서식을 사용하여 코드 구조를 강조하고 코드를 보다 쉽게 읽을 수 있도록 생성합니다. Microsoft 예제 및 샘플은 다음 규칙을 따릅니다.
기본 코드 편집기 설정(스마트 들여쓰기, 4자 들여쓰기, 탭을 공백으로 저장)을 사용합니다. 자세한 내용은 옵션, 텍스트 편집기, C#, 서식을 참조하세요.
문을 한 줄에 하나씩만 작성합니다.
선언을 한 줄에 하나씩만 작성합니다.
연속 줄이 자동으로 들여쓰기되지 않으면 탭 정지 1개(공백 4개)로 들여쓰기합니다.
메서드 정의와 속성 정의 간에는 빈 줄을 하나 이상 추가합니다.
다음 코드에 나와 있는 것처럼 괄호를 사용하여 식의 절을 명확하게 구분합니다.
if ((startX > endX) && (startX > previousX)) { // Take appropriate action. }
샘플에서 연산자 또는 식 우선순위를 설명하는 경우는 예외입니다.
보안
보안 코딩 지침의 지침을 따르세요.
.NET