개발관련/오류노트

NullReferenceException의 해결방법

Rateye 2021. 6. 26. 13:37
728x90
반응형
질문 : NullReferenceException이란 무엇이며 어떻게 해결합니까?

몇 가지 코드가 있으며 실행되면 NullReferenceException .

개체 참조가 개체의 인스턴스로 설정되지 않았습니다.

이것은 무엇을 의미하며이 오류를 수정하려면 어떻게해야합니까?

답변

null (또는 VB.NET에서는 Nothing 것을 사용하려고합니다. 이것은 당신이 그것을 null 설정했거나 아무것도 설정하지 않았 음을 의미합니다.

다른 것과 마찬가지로 null 이 전달됩니다. 메서드 "A" 에서 null 메서드 "B"가 메서드 "A"에 null

null 은 다른 의미를 가질 수 있습니다.

이 기사의 나머지 부분에서는 더 자세히 설명하고 많은 프로그래머가 종종 저지르는 실수로 인해 NullReferenceException 이 발생할 수 있습니다.

NullReferenceException 던지는 runtime 은 항상 동일한 것을 의미합니다. 참조를 사용하려고하고 참조가 초기화되지 않았거나 한 번 초기화되었지만 더 이상 초기화되지 않았습니다.

이는 참조가 null null 참조를 통해 멤버 (예 : 메서드)에 액세스 할 수 없음을 의미합니다. 가장 간단한 경우 :

string foo = null;
foo.ToUpper();

null 가리키는 string ToUpper() 를 호출 할 수 없기 때문에 두 번째 줄에서 NullReferenceException 이 발생합니다.

NullReferenceException 의 원인을 어떻게 찾습니까? 예외가 발생하는 위치에서 정확히 throw되는 예외 자체를 보는 것 외에도 Visual Studio에서 디버깅의 일반 규칙이 적용됩니다. 전략적 중단 점을 배치하고 변수를 검사합니다. 이름 위에 마우스를 올려 놓고 ( Quick) Watch window 또는 Locals 및 Autos와 같은 다양한 디버깅 패널 사용.

참조가 설정되어 있는지 여부를 확인하려면 해당 이름을 마우스 오른쪽 단추로 클릭하고 "모든 참조 찾기"를 선택합니다. 그런 다음 발견 된 모든 위치에 중단 점을 배치하고 연결된 디버거로 프로그램을 실행할 수 있습니다. 디버거가 이러한 중단 점에서 중단 될 때마다 참조가 null이 아닌 것으로 예상되는지 여부를 확인하고 변수를 검사하고 예상 할 때 인스턴스를 가리키는 지 확인해야합니다.

이러한 방식으로 프로그램 흐름을 따라 가면 인스턴스가 null이 아니어야하는 위치와 제대로 설정되지 않은 이유를 찾을 수 있습니다.

예외가 발생할 수있는 몇 가지 일반적인 시나리오 :

ref1.ref2.ref3.member

ref1 또는 ref2 또는 ref3이 null이면 NullReferenceException 합니다. 문제를 해결하려면 표현식을 더 간단한 등가물로 다시 작성하여 어떤 것이 null인지 확인하십시오.

var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member

특히 HttpContext.Current.User.Identity.Name 에서 HttpContext.Current 는 null이거나 User 속성이 null이거나 Identity 속성이 null 일 수 있습니다.

public class Person 
{
    public int Age { get; set; }
}
public class Book 
{
    public Person Author { get; set; }
}
public class Example 
{
    public void Foo() 
    {
        Book b1 = new Book();
        int authorAge = b1.Author.Age; // You never initialized the Author property.
                                       // there is no Person to get an Age from.
    }
}

자식 (Person) null 참조를 피하려면 부모 (Book) 개체의 생성자에서 초기화 할 수 있습니다.

중첩 된 개체 이니셜 라이저에도 동일하게 적용됩니다.

Book b1 = new Book
{
    Author = { Age = 45 }
};
                 

이것은 다음으로 번역됩니다.

Book b1 = new Book
{
    Author = { Age = 45 }
};

new 키워드가 사용되는 동안 Book 의 새 인스턴스 만 만들고 Person 의 새 인스턴스는 만들지 않으므로 Author the 속성은 여전히 null 입니다.

public class Person
    {
        public ICollection<Book> Books { get; set; }
    }
    public class Book
        {
            public string Title { get; set; }
    }
                                     

중첩 된 컬렉션 Initializers 는 동일하게 작동합니다.

public class Person
    {
        public ICollection<Book> Books { get; set; }
    }
    public class Book
        {
            public string Title { get; set; }
    }
                                                                                      

이것은 다음으로 번역됩니다.

Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });
                                                       

new Person Person 인스턴스 만 생성하지만 Books 컬렉션은 여전히 null 입니다. 컬렉션 Initializer p1.Books 대한 컬렉션을 생성하지 않으며 p1.Books.Add(...) 문으로 만 변환됩니다.

Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });
Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });
long[][] array = new long[1][];
array[0][0] = 3; // is null because only the first dimension is yet initialized.
// Use array[0] = new long[2]; first.
                                         
Dictionary<string, int> agesForNames = null;
int age = agesForNames["Bob"]; // agesForNames is null.
// There is no Dictionary to perform the lookup.
                                                      
public class Person
    {
        public string Name { get; set; }
    }
    var people = new List<Person>();
    people.Add(null);
    var names = from p in people select p.Name;
    string firstName = names.First(); // Exception is thrown here, but actually occurs
    // on the line above.  "p" is null because the
    // first element we added to the list is null.
                                                             
public class Demo
    {
        public event EventHandler StateChanged;

protected virtual void OnStateChanged(EventArgs e)
{
    StateChanged(this, e); // Exception is thrown here
                                                  // if no event handlers have been attached
                                                                                    // to StateChanged event
}
}

(참고 : VB.NET 컴파일러는 이벤트 사용에 대한 null 검사를 삽입하므로 VB.NET에서 Nothing

필드 이름을 현지인과 다르게 지정했다면 필드를 초기화하지 않았 음을 깨달았을 것입니다.

public class Form1
{
    private Customer customer;
    
    private void Form1_Load(object sender, EventArgs e) 
    {
        Customer customer = new Customer();
        customer.Name = "John";
    }
    
    private void Button_Click(object sender, EventArgs e)
    {
        MessageBox.Show(customer.Name);
    }
}

필드 앞에 밑줄을 붙이는 규칙에 따라이 문제를 해결할 수 있습니다.

    private Customer _customer;
                                                                                                                                                                                                                                                                                                                                                                                                                 
public partial class Issues_Edit : System.Web.UI.Page
{
    protected TestIssue myIssue;

protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Only called on first load, not when button clicked
myIssue = new TestIssue();
}
}

protected void SaveButton_Click(object sender, EventArgs e)
{
    myIssue.Entry = "NullReferenceException here!";
}
}
// if the "FirstName" session value has not yet been set,
// then this line will throw a NullReferenceException
string firstName = Session["FirstName"].ToString();
                                                           

ASP.NET MVC View 에서 @Model 속성을 참조 할 때 예외가 발생하면 return Model 이 작업 메서드에 설정된다는 것을 이해해야합니다. 컨트롤러에서 빈 모델 (또는 모델 속성)을 반환하면 뷰가 액세스 할 때 예외가 발생합니다.

// Controller
public class Restaurant:Controller
{
    public ActionResult Search()
{
return View();  // Forgot the provide a Model here.
}
}

// Razor view
         @foreach (var restaurantSearch in Model.RestaurantSearch)  // Throws.
{
}

<p>@Model.somePropertyName</p> <!-- Also throws -->
                                                    

WPF 컨트롤은 시각적 트리에 나타나는 순서대로 InitializeComponent 를 호출하는 동안 만들어집니다. NullReferenceException 은 이벤트 핸들러 등을 사용하여 초기에 생성 된 컨트롤의 경우에 발생하며, 이는 나중에 생성 된 컨트롤을 참조 InitializeComponent

예를 들면 :

<Grid>
<!-- Combobox declared first -->
<ComboBox Name="comboBox1"
Margin="10"
SelectedIndex="0"
SelectionChanged="comboBox1_SelectionChanged">
<ComboBoxItem Content="Item 1" />
<ComboBoxItem Content="Item 2" />
<ComboBoxItem Content="Item 3" />
</ComboBox>

<!-- Label declared later -->
<Label Name="label1"
Content="Label"
Margin="10" />
</Grid>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        

여기서 comboBox1 label1 이전에 생성됩니다. comboBox1_SelectionChanged 가`label1을 참조하려고하면 아직 생성되지 않은 것입니다.

private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    label1.Content = comboBox1.SelectedIndex.ToString(); // NullReferenceException here!!
}
                                            

XAML 에서 선언 순서를 변경하면 (즉, comboBox1 앞에 label1 나열, 디자인 철학의 문제 무시) NullReferenceException 해결됩니다.

var myThing = someObject as Thing;
                                       

이것은 InvalidCastException 던지지 않지만 캐스트가 실패 할 때 (그리고 someObject 가 자체적으로 null 일 null 반환합니다. 그러니 알아 두세요.

일반 버전 First()Single() 은 아무것도 없으면 예외를 발생시킵니다. 이 경우 "OrDefault"버전은 null 을 반환합니다. 그러니 알아 두세요.

foreach null 컬렉션을 반복하려고 할 때 발생합니다. 일반적으로 컬렉션을 반환하는 메서드의 null 결과로 인해 발생합니다.

List<int> list = null;
foreach(var v in list) { } // NullReferenceException here
                                                          

보다 현실적인 예-XML 문서에서 노드를 선택합니다. 노드를 찾을 수 없지만 초기 디버깅에 모든 속성이 유효한 것으로 표시되면 발생합니다.

foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))

null 일 것으로 예상되는 경우 인스턴스 멤버에 액세스하기 전에 null 확인할 수 있습니다.

void PrintName(Person p)
{
if (p != null)
{
    Console.WriteLine(p.Name);
}
}

인스턴스를 예상하여 호출하는 메서드는 예를 들어 찾고있는 개체를 찾을 수없는 경우 null 다음과 같은 경우 기본값을 반환하도록 선택할 수 있습니다.

string GetCategory(Book b)
{
if (b == null)
return "Unknown";
return b.Category;
}

또한 사용자 지정 예외를 throw 할 수 있으며 호출 코드에서만이를 포착 할 수 있습니다.

string GetCategory(string bookTitle)
{
    var book = library.FindBook(bookTitle);  // This may return null
if (book == null)
    throw new BookNotFoundException(bookTitle);  // Your custom exception
    return book.Category;
    }

null 을 반환 할 수는 있지만 절대로 반환해서는 안된다는 것을 알고있는 경우 Debug.Assert() 를 사용하여 발생하는 경우 가능한 한 빨리 중단 할 수 있습니다.

string GetTitle(int knownBookID)
{
// You know this should never return null.
var book = library.GetBook(knownBookID);

// Exception will occur on the next line instead of at the end of this method.
Debug.Assert(book != null, "Library didn't return a book for known book ID.");

// Some other code

return book.Title; // Will never throw NullReferenceException in Debug mode.
}

이 검사 는 릴리스 빌드에서 끝나지 않지만 릴리스 모드에서 런타임에 book == null 일 때 NullReferenceException 다시 throw합니다.

DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the default value provided (DateTime.Now), because appointment is null.

appointment = new DateTime(2022, 10, 20);
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the appointment date, not the default

null 이 발견 될 때 기본값을 제공하는 약어 :

DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the default value provided (DateTime.Now), because appointment is null.

appointment = new DateTime(2022, 10, 20);
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the appointment date, not the default

이것은 때로는 안전 탐색 또는 Elvis (모양 뒤에) 연산자라고도합니다. 연산자의 왼쪽에있는식이 null이면 오른쪽이 평가되지 않고 대신 null이 반환됩니다. 이는 다음과 같은 경우를 의미합니다.

var title = person.Title.ToUpper();

사람에게 직함이없는 경우 null 값이있는 속성에서 ToUpper 를 호출하려고하므로 예외가 발생합니다.

C# 5 이하에서는 다음과 같이 보호 할 수 있습니다.

var title = person.Title == null ? null : person.Title.ToUpper();

이제 제목 변수는 예외를 발생시키는 대신 null이됩니다. C # 6은 이에 대한 더 짧은 구문을 도입합니다.

var title = person.Title?.ToUpper();

그러면 title 변수가 null person.Titlenull ToUpper 대한 호출이 수행되지 않습니다.

물론, 당신은 여전히 확인해야 title 위해 null 또는 널 병합 연산자 (함께 널 조건 연산자를 사용 ?? 디폴트 값을 제공하기 위해) :

// regular null check
int titleLength = 0;
if (title != null)
    titleLength = title.Length; // If title is null, this would throw NullReferenceException

    // combining the `?` and the `??` operator
    int titleLength = title?.Length ?? 0;

마찬가지로 배열의 경우 다음과 같이 ?[i]

int[] myIntArray = null;
var i = 5;
int? elem = myIntArray?[i];
if (!elem.HasValue) Console.WriteLine("No value");

그러면 다음이 수행됩니다. myIntArraynull 표현식이 null 반환하므로 안전하게 확인할 수 있습니다. 배열이 포함 된 경우 다음과 같이 수행됩니다. elem = myIntArray[i]; i 번째 요소를 반환합니다.

도입 된 C# 8 , 널 컨텍스트 및 널 참조 유형은 정적 변수 분석을 수행하고 값이 잠재적 될 수 있다면 컴파일러 경고를 제공 null 또는 적이 설정 null . nullable 참조 형식을 사용하면 형식이 명시 적으로 null 이 될 수 있습니다.

nullable 주석 컨텍스트 및 nullable 경고 컨텍스트는 csproj 파일 Nullable 요소를 사용하여 프로젝트에 대해 설정할 수 있습니다. 이 요소는 컴파일러가 형식의 Null 허용 여부를 해석하는 방법과 생성되는 경고를 구성합니다. 유효한 설정은 다음과 같습니다.

  • enable : nullable 주석 컨텍스트가 활성화됩니다. nullable 경고 컨텍스트가 활성화되었습니다. 예를 들어 참조 유형의 변수 (문자열)는 널 (null)이 아닙니다. 모든 null 허용 여부 경고가 활성화됩니다.
  • disable : nullable 주석 컨텍스트가 비활성화됩니다. nullable 경고 컨텍스트가 비활성화되었습니다. 참조 형식의 변수는 이전 버전의 C #과 마찬가지로 알 수 없습니다. 모든 null 허용 여부 경고가 비활성화됩니다.
  • safeonly : nullable 주석 컨텍스트가 활성화됩니다. nullable 경고 컨텍스트는 safeonly입니다. 참조 유형의 변수는 널이 아닙니다. 모든 안전 무효 경고가 활성화됩니다.
  • warnings : nullable 주석 컨텍스트가 비활성화되었습니다. nullable 경고 컨텍스트가 활성화되었습니다. 참조 유형의 변수는 알 수 없습니다. 모든 null 허용 여부 경고가 활성화됩니다.
  • safeonlywarnings : nullable 주석 컨텍스트가 비활성화됩니다. nullable 경고 컨텍스트는 safeonly입니다. 참조 유형의 변수는 알 수 없습니다. 모든 안전 무효 경고가 활성화됩니다.

nullable 참조 형식은 nullable 값 형식과 동일한 구문을 사용하여 표시됩니다. a ? 변수 유형에 추가됩니다.

C# 은 "반복자 블록"(다른 인기있는 언어에서는 "생성자"라고 함)을 지원합니다. NullReferenceException 은 지연된 실행으로 인해 반복기 블록에서 디버깅하기가 특히 까다로울 수 있습니다.

public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
{
for (int i = 0; i < count; ++i)
yield return f.MakeFrob();
}
...
FrobFactory factory = whatever;
IEnumerable<Frobs> frobs = GetFrobs();
...
foreach(Frob frob in frobs) { ... }

경우 whatever 결과 null 다음 MakeFrob 발생합니다. 이제 올바른 방법은 다음과 같다고 생각할 수 있습니다.

// DON'T DO THIS
public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
{
if (f == null)
throw new ArgumentNullException("f", "factory must not be null");
for (int i = 0; i < count; ++i)
yield return f.MakeFrob();
}

왜 이것이 잘못 되었습니까? foreach 될 때까지 실제로 실행 되지 않기 때문입니다! GetFrobs 대한 호출은 단순히 반복 될 때 반복기 블록을 실행할 객체를 반환합니다.

null 검사를 작성 NullReferenceException 을 방지 할 수 NullArgumentException 을 호출 지점이 아닌 반복 지점으로 이동하면 디버그하기에 매우 혼란 스럽습니다 .

올바른 수정 사항은 다음과 같습니다.

// DO THIS
public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
{
// No yields in a public method that throws!
if (f == null)
throw new ArgumentNullException("f", "factory must not be null");
return GetFrobsForReal(f, count);
}
private IEnumerable<Frob> GetFrobsForReal(FrobFactory f, int count)
{
// Yields in a private method
Debug.Assert(f != null);
for (int i = 0; i < count; ++i)
    yield return f.MakeFrob();
    }

즉, iterator 블록 로직이있는 private helper 메서드와 null 검사를 수행하고 iterator를 반환하는 public surface 메서드를 만듭니다. 이제 GetFrobs 가 호출되면 즉시 null 검사가 수행되고 시퀀스가 반복 될 때 GetFrobsForReal

LINQ to Objects의 참조 소스를 살펴보면이 기술이 전체적으로 사용된다는 것을 알 수 있습니다. 작성하기가 약간 더 복잡하지만 nullity 오류를 훨씬 쉽게 디버깅 할 수 있습니다. 작성자의 편의가 아닌 호출자의 편의를 위해 코드를 최적화하십시오 .

C# 에는 이름에서 알 수 있듯이 메모리 안전 및 형식 안전을 제공하는 일반 안전 메커니즘이 적용되지 않기 때문에 매우 위험한 "안전하지 않은"모드가 있습니다. 메모리가 작동하는 방식을 철저하고 깊이 이해하지 않는 한 안전하지 않은 코드를 작성해서는 안됩니다 .

안전하지 않은 모드에서는 다음 두 가지 중요한 사실을 알고 있어야합니다.

  • 포인터 를 역 참조하면 널 참조 를 역 참조하는 것과 동일한 예외가 생성됩니다.
  • 유효하지 않은 널이 아닌 포인터를 역 참조하면 일부 상황에서 해당 예외가 발생할 수 있습니다.

그 이유를 이해하려면 .NET이 처음에 NullReferenceException 을 생성하는 방법을 이해하는 것이 좋습니다. (이러한 세부 정보는 Windows에서 실행되는 .NET에 적용되며 다른 운영 체제는 유사한 메커니즘을 사용합니다.)

Windows 에서 가상화됩니다. 각 프로세스는 운영 체제에서 추적하는 많은 메모리 "페이지"의 가상 메모리 공간을 얻습니다. 메모리의 각 페이지에는 사용 방법 (읽기, 쓰기, 실행 등)을 결정하는 플래그가 설정되어 있습니다. 가장 낮은 페이지는 "어떤 방식 으로든 사용되면 오류가 발생 함"으로 표시됩니다.

C# 의 null 포인터와 null 참조는 모두 내부적으로 숫자 0으로 표시되므로 해당 메모리 저장소로 역 참조를 시도하면 운영 체제에서 오류가 발생합니다. 그런 다음 .NET 런타임은이 오류를 감지하여 NullReferenceException 으로 변환합니다.

그렇기 때문에 null 포인터와 null 참조를 모두 역 참조하면 동일한 예외가 생성됩니다.

두 번째 요점은 어떻습니까? 가상 메모리의 최하위 페이지에있는 잘못된 포인터를 역 참조하면 동일한 운영 체제 오류가 발생하여 동일한 예외가 발생합니다.

왜 이것이 의미가 있습니까? 두 개의 int를 포함하는 구조체와 null과 같은 관리되지 않는 포인터가 있다고 가정합니다. 구조체에서 두 번째 int를 역 참조하려고하면 CLR 은 위치 0에있는 저장소에 액세스하지 않습니다. 위치 4의 저장소에 액세스합니다. 그러나 논리적으로 이것은 null을 통해 해당 주소에 도달하기 때문에 null 역 참조입니다.

안전하지 않은 코드로 작업 할 때 NullReferenceException 하는 경우 문제를 일으키는 포인터가 null 일 필요는 없다는 점에 유의하십시오. 가장 낮은 페이지의 모든 위치가 될 수 있으며이 예외가 생성됩니다.

출처 : https://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it
728x90
반응형