버그가 있는 C# 코드: C# 프로그래밍에서 가장 흔히 저지르는 10가지 실수

게시 됨: 2022-03-11

씨샤프 소개

C#은 Microsoft CLR(공용 언어 런타임)을 대상으로 하는 여러 언어 중 하나입니다. CLR을 대상으로 하는 언어는 언어 간 통합 및 예외 처리, 향상된 보안, 구성 요소 상호 작용을 위한 단순화된 모델, 디버깅 및 프로파일링 서비스와 같은 기능의 이점을 누릴 수 있습니다. 오늘날의 CLR 언어 중에서 C#은 Windows 데스크톱, 모바일 또는 서버 환경을 대상으로 하는 복잡하고 전문적인 개발 프로젝트에 가장 널리 사용됩니다.

C#은 강력한 형식의 개체 지향 언어입니다. C#의 엄격한 형식 검사는 컴파일 및 실행 시간 모두에서 일반적인 C# 프로그래밍 오류의 대부분을 가능한 한 빨리 보고하고 위치를 매우 정확하게 찾아냅니다. 이렇게 하면 유형 안전을 적용하는 보다 자유로운 언어에서 문제가 되는 작업이 발생한 후 오랫동안 발생할 수 있는 수수께끼 오류의 원인을 추적하는 것과 비교하여 C Sharp 프로그래밍에서 많은 시간을 절약할 수 있습니다. 그러나 많은 C# 코더가 무의식적으로(또는 부주의하게) 이 감지의 이점을 버리고 이 C# 자습서에서 논의된 몇 가지 문제로 이어집니다.

이 C 샤프 프로그래밍 튜토리얼 정보

이 튜토리얼은 C# 프로그래머가 저지르는 가장 흔한 C# 프로그래밍 실수 10가지 또는 피해야 할 문제에 대해 설명하고 도움을 제공합니다.

이 기사에서 논의된 대부분의 실수는 C#과 관련이 있지만 일부는 CLR을 대상으로 하거나 FCL(프레임워크 클래스 라이브러리)을 사용하는 다른 언어와도 관련이 있습니다.

일반적인 C# 프로그래밍 실수 #1: 값과 같은 참조 사용 또는 그 반대

C++ 및 기타 많은 언어의 프로그래머는 변수에 할당한 값이 단순히 값인지 아니면 기존 개체에 대한 참조인지를 제어하는 ​​데 익숙합니다. 그러나 C Sharp 프로그래밍에서는 개체를 인스턴스화하고 변수에 할당하는 프로그래머가 아니라 개체를 작성한 프로그래머가 결정을 내립니다. 이것은 C# 프로그래밍을 배우려고 하는 사람들에게 흔히 발생하는 "잡담"입니다.

사용 중인 객체가 값 유형인지 참조 유형인지 모르는 경우 몇 가지 놀라운 상황에 직면할 수 있습니다. 예를 들어:

 Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue

보시다시피 PointPen 개체는 모두 똑같은 방식으로 생성되었지만 새로운 X 좌표 값이 point2 에 할당되었을 때 point1 의 값은 변경되지 않은 반면, pen1 의 값은 새로운 색상이 할당되었을 수정되었습니다. pen2 . 따라서 point1point2 에는 각각 Point 개체의 복사본이 포함되어 있는 반면 pen1pen2 에는 동일한 Pen 개체에 대한 참조가 포함되어 있다고 추론할 수 있습니다. 그러나 이 실험을 하지 않고 어떻게 그것을 알 수 있습니까?

대답은 개체 유형의 정의를 살펴보는 것입니다(Visual Studio에서 개체 유형 이름 위에 커서를 놓고 F12 키를 눌러 쉽게 수행할 수 있음).

 public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type

위에 표시된 것처럼 C# 프로그래밍에서 struct 키워드는 값 형식을 정의하는 데 사용되는 반면 class 키워드는 참조 형식을 정의하는 데 사용됩니다. C++ 배경 지식을 가진 사람들은 C++와 C# 키워드 사이의 많은 유사성으로 인해 잘못된 보안 감각에 잠겼습니다. 이 동작은 C# 자습서에서 도움을 요청하게 될 수도 있는 놀라운 일입니다.

객체를 메소드 매개변수로 전달하고 해당 메소드가 객체의 상태를 변경하도록 하는 기능과 같이 값 유형과 참조 유형 간에 다른 일부 동작에 의존하려는 경우 C# 프로그래밍 문제를 피하기 위해 올바른 유형의 개체.

일반적인 C# 프로그래밍 실수 #2: 초기화되지 않은 변수의 기본값에 대한 오해

C#에서 값 형식은 null일 수 없습니다. 정의에 따르면 값 유형에는 값이 있으며 값 유형의 초기화되지 않은 변수에도 값이 있어야 합니다. 이를 해당 유형의 기본값이라고 합니다. 이로 인해 변수가 초기화되지 않았는지 확인할 때 다음과 같은 일반적으로 예기치 않은 결과가 발생합니다.

 class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }

point1 이 null이 아닌 이유는 무엇입니까? 대답은 Point 가 값 유형이고 Point 의 기본값이 null이 아니라 (0,0)이라는 것입니다. 이것을 인식하지 못하는 것은 C#에서 저지르기 쉬운(그리고 흔한) 실수입니다.

모든 값은 아니지만 많은 값 유형에는 기본값과 같은지 확인할 수 있는 IsEmpty 속성이 있습니다.

 Console.WriteLine(point1.IsEmpty); // True

변수가 초기화되었는지 여부를 확인할 때 해당 유형의 초기화되지 않은 변수에 기본적으로 어떤 값이 있는지 확인하고 null에 의존하지 마십시오.

일반적인 C# 프로그래밍 실수 #3: 부적절하거나 지정되지 않은 문자열 비교 방법 사용

C#에서 문자열을 비교하는 방법에는 여러 가지가 있습니다.

많은 프로그래머가 문자열 비교를 위해 == 연산자를 사용하지만 실제로는 가장 바람직하지 않은 방법 중 하나입니다. 주로 코드에서 원하는 비교 유형을 명시적으로 지정하지 않기 때문입니다.

오히려 C# 프로그래밍에서 문자열 동등성을 테스트하는 선호되는 방법은 Equals 메서드를 사용하는 것입니다.

 public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);

첫 번째 메서드 서명(즉, comparisonType 매개 변수 없음)은 실제로 == 연산자를 사용하는 것과 동일하지만 문자열에 명시적으로 적용된다는 이점이 있습니다. 기본적으로 바이트 단위 비교인 문자열의 서수 비교를 수행합니다. 많은 경우에 이것은 정확히 원하는 비교 유형입니다. 특히 파일 이름, 환경 변수, 속성 등과 같이 값이 프로그래밍 방식으로 설정된 문자열을 비교할 때 그렇습니다. 이러한 경우 서수 비교가 실제로 올바른 유형인 한 해당 상황에 대한 비교의 경우, comparisonType 유형 없이 Equals 메서드를 사용하는 경우의 유일한 단점은 코드를 읽는 누군가가 어떤 유형의 비교를 수행하는지 모를 수 있다는 것입니다.

하지만 문자열을 비교할 때마다 comparisonType 이 포함된 Equals 메서드 서명을 사용하면 코드가 더 명확해질 뿐만 아니라 어떤 유형의 비교를 수행해야 하는지 명시적으로 생각하게 됩니다. 영어가 서수 비교와 문화에 민감한 비교 간에 많은 차이를 제공하지 않더라도 다른 언어는 충분히 제공하고 다른 언어의 가능성을 무시하면 오류가 발생합니다. 예를 들어:

 string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));

가장 안전한 방법은 항상 Equals 메서드에 comparisonType 매개 변수를 제공하는 것입니다. 다음은 몇 가지 기본 지침입니다.

  • 사용자가 입력했거나 사용자에게 표시할 문자열을 비교할 때 문화권 구분 비교( CurrentCulture 또는 CurrentCultureIgnoreCase )를 사용하십시오.
  • 프로그래밍 방식의 문자열을 비교할 때 순서 비교( Ordinal 또는 OrdinalIgnoreCase )를 사용하십시오.
  • InvariantCultureInvariantCultureIgnoreCase 는 일반적으로 매우 제한된 상황을 제외하고는 사용되지 않습니다. 서수 비교가 더 효율적이기 때문입니다. 문화권 인식 비교가 필요한 경우 일반적으로 현재 문화권 또는 다른 특정 문화권에 대해 수행해야 합니다.

Equals 메서드 외에도 문자열은 같음 테스트 대신 문자열의 상대적 순서에 대한 정보를 제공하는 Compare 메서드도 제공합니다. 이 방법은 C# 문제를 피하기 위해 위에서 설명한 것과 같은 이유로 < , <= , >>= 연산자보다 선호됩니다.

관련: 12가지 필수 .NET 인터뷰 질문

일반적인 C# 프로그래밍 실수 #4: 반복적(선언적 대신) 문을 사용하여 컬렉션 조작

C# 3.0에서 언어에 LINQ(Language-Integrated Query)를 추가하면 컬렉션을 쿼리하고 조작하는 방식이 완전히 변경되었습니다. 그 이후로 컬렉션을 조작하기 위해 반복 문을 사용하는 경우 LINQ를 사용해야 할 때 사용하지 않았습니다.

일부 C# 프로그래머는 LINQ의 존재조차 알지 못하지만 다행히 그 숫자는 점점 줄어들고 있습니다. 그러나 많은 사람들은 여전히 ​​LINQ 키워드와 SQL 문 간의 유사성 때문에 데이터베이스를 쿼리하는 코드에서만 사용된다고 생각합니다.

데이터베이스 쿼리는 LINQ 문을 매우 널리 사용하지만 실제로는 모든 열거 가능한 컬렉션(즉, IEnumerable 인터페이스를 구현하는 모든 개체)에서 작동합니다. 예를 들어, 계정 배열이 있는 경우 foreach에 대해 C# 목록을 작성하는 대신:

 decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }

다음과 같이 작성할 수 있습니다.

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

이것은 이 일반적인 C# 프로그래밍 문제를 피하는 방법에 대한 매우 간단한 예이지만 단일 LINQ 문으로 코드의 반복 루프(또는 중첩 루프)에 있는 수십 개의 문을 쉽게 바꿀 수 있는 경우가 있습니다. 그리고 일반적인 코드가 적다는 것은 버그가 도입될 기회가 적다는 것을 의미합니다. 그러나 성능 측면에서 절충점이 있을 수 있음을 명심하십시오. 성능이 중요한 시나리오, 특히 반복 코드가 LINQ가 할 수 없는 컬렉션에 대해 가정할 수 있는 경우 두 메서드 간에 성능 비교를 수행해야 합니다.

일반적인 C# 프로그래밍 실수 #5: LINQ 문에서 기본 개체를 고려하지 못함

LINQ는 메모리 내 개체, 데이터베이스 테이블 또는 XML 문서에 관계없이 컬렉션 조작 작업을 추상화하는 데 적합합니다. 완벽한 세계에서는 기본 개체가 무엇인지 알 필요가 없습니다. 그러나 여기서 오류는 우리가 완벽한 세상에 살고 있다고 가정하는 것입니다. 실제로 동일한 LINQ 문은 동일한 데이터에서 실행될 때 데이터 형식이 다른 경우 다른 결과를 반환할 수 있습니다.

예를 들어 다음 진술을 고려하십시오.

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

개체의 account.Status 중 하나가 "활성"(대문자 A 참고)과 같으면 어떻게 됩니까? 글쎄, myAccountsDbSet 개체(기본 대소문자를 구분하지 않는 구성으로 설정됨)인 경우 where 식은 여전히 ​​해당 요소와 일치합니다. 그러나 myAccounts 가 메모리 내 배열에 있는 경우 일치하지 않으므로 합계에 대해 다른 결과가 생성됩니다.

하지만 잠시만요. 이전에 문자열 비교에 대해 이야기했을 때 == 연산자가 문자열의 서수 비교를 수행하는 것을 보았습니다. 그렇다면 이 경우에 == 연산자가 대소문자를 구분하지 않는 비교를 수행하는 이유는 무엇입니까?

대답은 LINQ 문의 기본 개체가 SQL 테이블 데이터에 대한 참조인 경우(이 예에서 Entity Framework DbSet 개체의 경우와 같이) 해당 명령문이 T-SQL 문으로 변환된다는 것입니다. 그런 다음 연산자는 C# 프로그래밍 규칙이 아닌 T-SQL 프로그래밍 규칙을 따르므로 위의 경우 비교는 대소문자를 구분하지 않습니다.

일반적으로 LINQ는 개체 컬렉션을 쿼리하는 데 유용하고 일관된 방법이지만 실제로는 코드 동작이 다음과 같은지 확인하기 위해 문이 내부적으로 C#이 아닌 다른 것으로 변환되는지 여부를 여전히 알아야 합니다. 런타임 시 예상대로 됩니다.

일반적인 C# 프로그래밍 실수 #6: 확장 메서드에 의해 혼동되거나 속임수

앞에서 언급했듯이 LINQ 문은 IEnumerable을 구현하는 모든 개체에서 작동합니다. 예를 들어, 다음의 간단한 함수는 모든 계정 모음의 잔액을 합산합니다.

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }

위의 코드에서 myAccounts 매개변수의 유형은 IEnumerable<Account> 로 선언됩니다. myAccountsSum 메서드를 참조하므로(C#은 친숙한 "점 표기법"을 사용하여 클래스 또는 인터페이스의 메서드를 참조함) IEnumerable<T> 인터페이스 정의에서 Sum() 이라는 메서드를 볼 것으로 예상됩니다. 그러나 IEnumerable<T> 의 정의는 Sum 메서드를 참조하지 않으며 단순히 다음과 같습니다.

 public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }

그렇다면 Sum() 메서드는 어디에 정의되어 있습니까? C#은 강력한 형식이므로 Sum 메서드에 대한 참조가 유효하지 않은 경우 C# 컴파일러는 확실히 이를 오류로 플래그 지정합니다. 그러므로 우리는 그것이 존재해야 한다는 것을 압니다. 그러나 어디에 있습니까? 또한 이러한 컬렉션을 쿼리하거나 집계하기 위해 LINQ에서 제공하는 다른 모든 메서드의 정의는 어디에 있습니까?

대답은 Sum()IEnumerable 인터페이스에 정의된 메서드가 아니라는 것입니다. 오히려 System.Linq.Enumerable 클래스에 정의된 정적 메서드("확장 메서드"라고 함)입니다.

 namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }

그렇다면 확장 메서드가 다른 정적 메서드와 다른 점은 무엇이며 다른 클래스에서 확장 메서드에 액세스할 수 있도록 하는 것은 무엇입니까?

확장 메소드의 구별되는 특성은 첫 번째 매개변수의 this 수정자입니다. 이것은 컴파일러에서 확장 메서드로 식별하는 "마법"입니다. 수정하는 매개 변수의 유형(이 경우 IEnumerable<TSource> )은 이 메서드를 구현하는 것으로 나타날 클래스 또는 인터페이스를 나타냅니다.

(참고로 IEnumerable 인터페이스의 이름과 확장 메서드가 정의된 Enumerable 클래스의 이름 사이의 유사성에 대해 마법 같은 것은 없습니다. 이 유사성은 단지 임의의 문체 선택일 뿐입니다.)

이러한 이해를 바탕으로 위에서 소개한 sumAccounts 함수가 대신 다음과 같이 구현될 수 있음을 알 수 있습니다.

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }

우리가 이것을 이런 식으로 구현할 수 있었다는 사실은 왜 확장 메소드가 전혀 없는지에 대한 질문을 제기합니다. 확장 메서드는 기본적으로 새 파생 형식을 만들거나, 다시 컴파일하거나, 원래 형식을 수정하지 않고도 기존 형식에 메서드를 "추가"할 수 있는 C# 프로그래밍 언어의 편의입니다.

확장 메서드는 using [namespace]; 파일 맨 위에 있는 명령문. 찾고 있는 확장 메서드가 포함된 C# 네임스페이스를 알아야 하지만 검색하려는 것이 무엇인지 알게 되면 매우 쉽게 결정할 수 있습니다.

C# 컴파일러가 개체의 인스턴스에서 메서드 호출을 발견하고 참조된 개체 클래스에 정의된 해당 메서드를 찾지 못하면 범위 내에 있는 모든 확장 메서드를 살펴보고 필요한 메서드와 일치하는 메서드를 찾습니다. 서명 및 클래스. 하나를 찾으면 인스턴스 참조를 해당 확장 메서드에 대한 첫 번째 인수로 전달하고 나머지 인수(있는 경우)는 확장 메서드에 후속 인수로 전달됩니다. (C# 컴파일러가 범위 내에서 해당 확장 메서드를 찾지 못하면 오류가 발생합니다.)

확장 메서드는 C# 컴파일러 측에서 "구문 설탕"의 한 예이며, 이를 통해 (일반적으로) 더 명확하고 유지 관리하기 쉬운 코드를 작성할 수 있습니다. 더 명확합니다. 즉, 사용법을 알고 있는 경우입니다. 그렇지 않으면 특히 처음에는 약간 혼란스러울 수 있습니다.

확장 방법을 사용하면 확실히 이점이 있지만, 이를 인식하지 못하거나 제대로 이해하지 못하는 개발자에게 문제와 C# 프로그래밍 도움에 대한 외침을 유발할 수 있습니다. 이는 코드 샘플을 온라인으로 볼 때나 미리 작성된 다른 코드를 볼 때 특히 그렇습니다. 그러한 코드가 컴파일러 오류를 생성할 때(호출되는 클래스에 명확하게 정의되지 않은 메서드를 호출하기 때문에), 코드가 다른 버전의 라이브러리에 적용되거나 완전히 다른 라이브러리에 적용된다고 생각하는 경향이 있습니다. 존재하지 않는 새 버전 또는 팬텀 "누락된 라이브러리"를 찾는 데 많은 시간을 할애할 수 있습니다.

확장 메서드에 익숙한 개발자라도 개체에 같은 이름의 메서드가 있지만 메서드 서명이 확장 메서드의 메서드 시그니처와 미묘한 차이가 있는 경우 여전히 가끔 잡히는 경우가 있습니다. 거기에 없는 오타나 오류를 찾는 데 많은 시간이 낭비될 수 있습니다.

C# 라이브러리에서 확장 메서드를 사용하는 것이 점점 더 보편화되고 있습니다. LINQ 외에도 Unity 애플리케이션 블록 및 웹 API 프레임워크는 확장 메서드를 사용하는 Microsoft에서 많이 사용하는 두 가지 최신 라이브러리의 예이며 기타 여러 가지가 있습니다. 프레임워크가 현대적일수록 확장 방법을 통합할 가능성이 높아집니다.

물론 자신만의 확장 메서드를 작성할 수도 있습니다. 그러나 확장 메서드가 일반 인스턴스 메서드처럼 호출되는 것처럼 보이지만 실제로는 환상일 뿐입니다. 특히 확장 메서드는 확장 중인 클래스의 private 또는 protected 멤버를 참조할 수 없으므로 보다 전통적인 클래스 상속을 완전히 대체할 수 없습니다.

일반적인 C# 프로그래밍 실수 #7: 당면한 작업에 잘못된 유형의 컬렉션 사용

C#은 매우 다양한 컬렉션 개체를 제공하며 다음은 일부 목록일 뿐입니다.
Array , ArrayList , BitArray , BitVector32 , Dictionary<K,V> , HashTable , HybridDictionary , List<T> , NameValueCollection , OrderedDictionary , Queue, Queue<T> , SortedList , Stack, Stack<T> , StringCollection , StringDictionary .

너무 많은 선택이 충분하지 않은 것만큼 나쁜 경우가 있을 수 있지만 컬렉션 개체의 경우는 그렇지 않습니다. 사용 가능한 옵션의 수는 확실히 귀하에게 유리하게 작용할 수 있습니다. 사전에 약간의 시간을 할애하여 목적에 맞는 최적의 수집 유형을 조사하고 선택하십시오. 결과적으로 성능이 향상되고 오류가 발생할 여지가 줄어듭니다.

가지고 있는 요소 유형(예: 문자열 또는 비트)을 특별히 대상으로 하는 컬렉션 유형이 있는 경우 해당 유형을 먼저 사용하는 것이 좋습니다. 구현은 일반적으로 특정 유형의 요소를 대상으로 할 때 더 효율적입니다.

C#의 형식 안전성을 활용하려면 일반적으로 제네릭이 아닌 인터페이스보다 제네릭 인터페이스를 선호해야 합니다. 제네릭 인터페이스의 요소는 개체를 선언할 때 지정하는 형식인 반면 비제네릭 인터페이스의 요소는 개체 형식입니다. 제네릭이 아닌 인터페이스를 사용하는 경우 C# 컴파일러는 코드를 유형 검사할 수 없습니다. 또한 기본 값 형식의 컬렉션을 처리할 때 제네릭이 아닌 컬렉션을 사용하면 해당 형식의 박싱/언박싱이 반복되어 적절한 형식의 제네릭 컬렉션과 비교할 때 성능에 심각한 부정적인 영향을 줄 수 있습니다.

또 다른 일반적인 C# 문제는 고유한 컬렉션 개체를 작성하는 것입니다. 그것이 결코 적절하지 않다는 것은 아니지만 .NET이 제공하는 것과 같은 포괄적인 선택을 통해 바퀴를 재발명하는 대신 이미 존재하는 것을 사용하거나 확장하여 많은 시간을 절약할 수 있습니다. 특히 C# 및 CLI용 C5 일반 컬렉션 라이브러리는 영구 트리 데이터 구조, 힙 기반 우선 순위 대기열, 해시 인덱스 배열 목록, 연결 목록 등과 같은 "즉시 사용 가능한" 다양한 추가 컬렉션을 제공합니다.

일반적인 C# 프로그래밍 실수 #8: 무료 리소스 무시

CLR 환경은 가비지 수집기를 사용하므로 개체에 대해 생성된 메모리를 명시적으로 해제할 필요가 없습니다. 사실, 당신은 할 수 없습니다. C++ delete 연산자나 C의 free() 함수에 해당하는 것은 없습니다. 그러나 그렇다고 해서 모든 개체를 사용한 후에 잊어도 된다는 의미는 아닙니다. 많은 유형의 개체가 다른 유형의 시스템 리소스(예: 디스크 파일, 데이터베이스 연결, 네트워크 소켓 등)를 캡슐화합니다. 이러한 리소스를 열어 두면 전체 시스템 리소스 수가 빠르게 고갈되어 성능이 저하되고 궁극적으로 프로그램 오류가 발생할 수 있습니다.

소멸자 메서드는 모든 C# 클래스에서 정의할 수 있지만 소멸자(C#에서는 종료자라고도 함)의 문제는 언제 호출될지 확실히 알 수 없다는 것입니다. 미래의 불확실한 시간에 가비지 수집기(별도의 스레드에서 추가 문제를 일으킬 수 있음)에 의해 호출됩니다. GC.Collect() 를 사용하여 가비지 수집을 강제로 사용하여 이러한 제한을 해결하려는 시도는 C# 모범 사례가 아닙니다. 이렇게 하면 수집할 수 있는 모든 개체를 수집하는 동안 알 수 없는 시간 동안 스레드가 차단되기 때문입니다.

이것은 종료자의 좋은 용도가 없다는 말은 아니지만 결정적인 방식으로 리소스를 해제하는 것은 그 중 하나가 아닙니다. 오히려 파일, 네트워크 또는 데이터베이스 연결에서 작업할 때 작업이 완료되는 즉시 기본 리소스를 명시적으로 해제하려고 합니다.

리소스 누출은 거의 모든 환경에서 문제가 됩니다. 그러나 C#은 강력하고 사용이 간편한 메커니즘을 제공하며, 이를 활용하면 누수가 훨씬 더 드물게 발생할 수 있습니다. .NET 프레임워크는 Dispose() 메서드로만 구성된 IDisposable 인터페이스를 정의합니다. IDisposable 을 구현하는 모든 개체는 개체의 소비자가 개체 조작을 완료할 때마다 해당 메서드가 호출될 것으로 예상합니다. 그 결과 명시적이고 결정적인 리소스 해제가 발생합니다.

단일 코드 블록의 컨텍스트 내에서 객체를 생성 및 폐기하는 경우 기본적 using Dispose() Dispose() 을 잊어버리는 것은 변명의 여지가 없습니다. 종료됩니다(예외, return 문 또는 단순히 블록 닫기). 그리고 예, 파일 맨 위에 C# 네임스페이스를 포함하는 데 사용되는 앞에서 언급한 using 문과 동일합니다. 많은 C# 개발자가 인식하지 못하는 완전히 관련 없는 두 번째 목적이 있습니다. 즉, 코드 블록이 종료될 때 객체에 대해 Dispose() 가 호출되도록 하려면:

 using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }

위의 예제에서 using 블록을 생성하면 Read() 가 예외를 throw하는지 여부에 관계없이 파일 작업이 완료되는 즉시 myFile.Dispose() 가 호출된다는 것을 알 수 있습니다.

일반적인 C# 프로그래밍 실수 #9: 예외를 피하는 것

C#은 런타임에 형식 안전성을 계속 적용합니다. 이렇게 하면 잘못된 형식 변환으로 인해 개체의 필드에 임의의 값이 할당될 수 있는 C++와 같은 언어보다 훨씬 빠르게 C#의 여러 유형의 오류를 찾아낼 수 있습니다. 그러나 다시 한 번 프로그래머는 이 훌륭한 기능을 낭비하여 C# 문제를 일으킬 수 있습니다. C#은 예외를 던질 수 있는 것과 하지 않는 일의 두 가지 다른 방법을 제공하기 때문에 이 함정에 빠지게 됩니다. 일부는 try/catch 블록을 작성할 필요가 없어 코딩을 절약할 수 있다고 생각하여 예외 경로를 피합니다.

예를 들어 다음은 C#에서 명시적 형식 캐스트를 수행하는 두 가지 방법입니다.

 // METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;

방법 2를 사용할 때 발생할 수 있는 가장 명백한 오류는 반환 값을 확인하지 못하는 것입니다. 그러면 나중에 NullReferenceException이 발생할 수 있으며, 이는 나중에 나타날 수 있어 문제의 원인을 추적하기가 훨씬 더 어려워집니다. 대조적으로, 방법 1은 즉시 InvalidCastException 을 발생시켜 문제의 원인을 훨씬 더 즉각적으로 명확하게 했습니다.

또한, 방법 2에서 반환 값을 확인하는 것을 기억하더라도 null이 발견되면 어떻게 하시겠습니까? 작성하고 있는 방법이 오류를 보고하기에 적절한 위치입니까? 해당 캐스트가 실패하면 시도할 수 있는 다른 것이 있습니까? 그렇지 않은 경우 예외를 발생시키는 것이 올바른 일이므로 가능한 한 문제의 원인에 가깝게 발생하도록 두는 것이 좋습니다.

다음은 하나는 예외를 throw하고 다른 하나는 예외를 throw하지 않는 다른 일반적인 방법 쌍의 몇 가지 예입니다.

 int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty

일부 C# 개발자는 예외를 발생시키지 않는 방법이 더 우수하다고 자동으로 가정할 정도로 "예외에 반대"합니다. 이것이 사실일 수 있는 특정 선택 사례가 있지만 일반화로서 전혀 정확하지 않습니다.

구체적인 예로서, 예외가 생성되었을 때 취할 대안적 합법적(예: 기본) 조치가 있는 경우 비예외 접근 방식이 합법적인 선택이 될 수 있습니다. 그런 경우에는 다음과 같이 작성하는 것이 더 나을 수 있습니다.

 if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }

대신에:

 try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }

그러나 TryParse 가 반드시 "더 나은" 방법이라고 가정하는 것은 올바르지 않습니다. 그럴 때도 있고 아닐 때도 있습니다. 그렇기 때문에 두 가지 방법이 있습니다. 예외가 개발자의 친구가 될 수 있음을 기억하면서 현재 상황에 맞는 올바른 것을 사용하십시오.

일반적인 C# 프로그래밍 실수 #10: 컴파일러 경고 누적 허용

이 문제는 확실히 C#에만 국한되지는 않지만 C# 컴파일러에서 제공하는 엄격한 유형 검사의 이점을 포기하기 때문에 C# 프로그래밍에서 특히 심각합니다.

이유 때문에 경고가 생성됩니다. 모든 C# 컴파일러 오류는 코드의 결함을 나타내지만 많은 경고도 마찬가지입니다. 이 둘을 구별하는 것은 경고의 경우 컴파일러가 코드가 나타내는 명령을 내보내는 데 문제가 없다는 것입니다. 그럼에도 불구하고 코드가 약간 어색하고 코드가 의도를 정확하게 반영하지 않을 가능성이 있습니다.

이 C# 프로그래밍 자습서를 위한 일반적인 간단한 예는 알고리즘을 수정하여 사용하던 변수의 사용을 제거했지만 변수 선언을 제거하는 것을 잊어버린 경우입니다. 프로그램은 완벽하게 실행되지만 컴파일러는 쓸모없는 변수 선언에 플래그를 지정합니다. 프로그램이 완벽하게 실행된다는 사실은 프로그래머가 경고의 원인을 수정하는 것을 소홀히 하게 만듭니다. 또한 코더는 Visual Studio 기능을 활용하여 "오류 목록" 창에서 경고를 쉽게 숨길 수 있으므로 오류에만 집중할 수 있습니다. 수십 개의 경고가 있을 때까지 오랜 시간이 걸리지 않으며, 모두 행복하게 무시됩니다(더 나쁘게는 숨겨져 있음).

그러나 이러한 유형의 경고를 무시하면 조만간 다음과 같은 내용이 코드에 들어갈 수 있습니다.

 class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }

그리고 Intellisense를 통해 코드를 작성할 수 있는 속도로 이 오류는 보이는 것만큼 가능성이 높지 않습니다.

이제 프로그램에 심각한 오류가 있습니다(컴파일러가 이미 설명한 이유 때문에 경고로만 표시했지만). 프로그램이 얼마나 복잡한지에 따라 이 오류를 추적하는 데 많은 시간을 낭비할 수 있습니다. 처음에 이 경고에 주의를 기울였다면 간단한 5초 수정으로 이 문제를 피할 수 있었을 것입니다.

C Sharp 컴파일러는 코드의 견고성에 대한 유용한 정보를 많이 제공한다는 것을 기억하십시오. 듣고 계시다면… 경고를 무시하지 마십시오. 일반적으로 수정하는 데 몇 초 밖에 걸리지 않으며 새로운 문제가 발생했을 때 수정하면 시간을 절약할 수 있습니다. Visual Studio의 "오류 목록" 창에 "0 오류, 0 경고"가 표시될 것으로 예상하도록 자신을 훈련하십시오.

물론 모든 규칙에는 예외가 있습니다. 따라서 의도한 대로 코드가 컴파일러에게 다소 어색해 보일 수 있습니다. 매우 드문 경우지만 경고를 트리거하는 코드 주위에, 그리고 그것이 트리거하는 경고 ID에만 #pragma warning disable [warning id] 를 사용하십시오. 이렇게 하면 해당 경고 및 해당 경고만 표시되지 않으므로 새 경고에 대해 계속 경계할 수 있습니다.

마무리

C#은 생산성을 크게 향상시킬 수 있는 많은 메커니즘과 패러다임을 가진 강력하고 유연한 언어입니다. 그러나 다른 소프트웨어 도구나 언어와 마찬가지로 그 기능을 제한적으로 이해하거나 이해하는 것은 때때로 이점보다 더 큰 장애물이 될 수 있으며, "위험할 만큼 충분히 안다"는 속담 상태가 됩니다.

이와 같은 C Sharp 자습서를 사용하여 이 기사에서 제기된 문제와 같은 C#의 주요 뉘앙스를 익히면 C# 최적화에 도움이 되며 C# 최적화에 도움이 됩니다. 언어.


Toptal 엔지니어링 블로그에 대한 추가 정보:

  • 필수 C# 인터뷰 질문
  • C# 대 C++: 핵심은 무엇입니까?