C# 초보 메서드 사용법 질문 드립니다

우선 저는 이제 C# 문법을 조금 익히고 연습 중인 사람입니다.
앞으로 선배 분들에게 많은 도움 요청 드리겠습니다.
잘 부탁 드립니다.


첫 질문은 이렇습니다.

string도 참조형 멤버이고 Bitmap도 참조형 멤버라 생각했는데,

아래 예1) string은 메서드에 전달한 후에 값이 메서드 호출과 상관없이 원래 a값이 출력 되는데
예2) 에서는 리턴도 받지 않았는데 메서드에서 처리한 결과 값이 출력 되는데 제가 아직 참조형에 대해서
잘못 이해하고 있는 것 같은데 알려주시면 감사 드리겠습니다.

앞으로 하고 싶은 질문이 너무 많은데 오늘은 짧게 한 가지 질문 드립니다.

예1)

string a= "Hello World"
method1(a);
textBox1.Text = a

private void method1(string b)
{
    b="test"
}

예2)

Bitmap a = new Bitmap(xxx,xxx)
Gray_image(a)
pictureBox1.image = a

private void Gray_image(Bitmap b)
{
    // b의 픽셀을 변경하는 코드
    // 리턴값 없음
}
1개의 좋아요

string과 Bitmap 모두 참조형이 맞습니다. 그런데 예1은 원래 값이 출력이 되고, 예2는 b의 픽셀값을 변경하는 코드가 적용이 되어 원래 값이 아닌 적용된 최종 형태가 되는데요,

차이점이 있습니다.

예1)에서 b에 "test"를 대입했을 때, 밖의 a를 변경한게 아니라 메소드 인자로 전달된 b가 변경된 것입니다.

그런데 예2)에서는 메소드 인자로 전달된 b 자체를 변경한게 아니라 b의 속성 또는 메소드를 통해 b가 바라보는 값을 변경한 것입니다.

2개의 좋아요

이걸 말로 설명하려 하면 참 힘든데, 그림으로 그리면 명확해집니다.

아래 그림을 참고해보세요. M'b란 표현은 메소드의 b 변수란 의미입니다.

3개의 좋아요

2개의 좋아요

@dimohy 님 설명에서 조금 더 설명을 덧붙이자면, 예 1은 string 특성을 같이 이해하면 더 쉬울거에요.
string으로 값을 초기화 한 경우, 그 자체로 하나의 메모리 주소를 갖습니다.
즉 아래 두 개는 달라요.

string str1 = "Hello World";
str1 += ", Hello C#";
// 변수명은 같지만 실제 할당된 메모리 주소는 다름

아래 코드로 실험해보셔도 좋습니다.

string a = "Hello World";
PrintPtr("a", ref a);
method1(a);
PrintPtr("a", ref a);

void method1(string b)
{
	PrintPtr("b", ref b);
	b = "test";
	PrintPtr("b", ref b);
}

void PrintPtr<T>(string name, ref T obj)
{
	var objHandle = GCHandle.Alloc(obj, GCHandleType.Pinned);
	var address = objHandle.AddrOfPinnedObject();
	Console.WriteLine($"{name} : {address}");
}

위 코드의 결과는 아래처럼 나올거에요.

image

스택오버플로우에 비슷한 내용이 있는데 이 답변과 같이 보시면 좋을 것 같습니다.

2개의 좋아요

답글 감사드립니다.
string 에 값을 넣을때 결국 new로 새로운 클래스를 만드는것과 같은거라고 볼수 있나요?

1개의 좋아요

답글 감사드립니다.
그림까지 그려주셔서 너무 감사드려요~~

1개의 좋아요

아… 조금 다릅니다. 기본적으로 예라고 말씀드릴 수 있고, 세부적인 동작은 아니라고 말씀드릴 수 있습니다.

“Hello World” 형태를 문자열 리터럴이라 하는데요, 힙 메모리에 위치하지 않습니다. 이 문자열은 특별하게 컴파일 되었을 때 실행파일의 리소스가 되며, 실행 파일이 실행될 때 같이 코드와 함께 메모리로 적재되며, 실행이 종료될 때까지 그 메모리 주소 위치가 변하지 않습니다.

@level120 님의 소스코드를 응용해서 다음을 확인할 수 있습니다.

using System.Runtime.InteropServices;

var a = "Hello, world!";
PrintPtr("a", ref a);
var b = a;
PrintPtr("b", ref b);
var c = "Hello, world!";
PrintPtr("c", ref c);


void PrintPtr<T>(string name, ref T obj)
{
	var objHandle = GCHandle.Alloc(obj, GCHandleType.Pinned);
	var address = objHandle.AddrOfPinnedObject();
	Console.WriteLine($"{name} : {address}");
}

결과

a : 2408203814924
b : 2408203814924
c : 2408203814924

ab의 메모리 주소가 같은 것은 이해됩니다. var b = a;를 통해 같은 위치를 바라보게 되었기 때문입니다. 그런데 c까지도 메모리 주소가 같네요?
C# string는 문자 리터럴을 통해 string을 생성할 경우, 동일한 문자 리터럴일 경우 그 개체를 공유합니다.

1개의 좋아요

음… 이걸 진짜 개념적으루 쉽게 이해한다면… 이렇게 보면 됩니다.

C# 에서 메서드에 parameter를 넘기는 동작은 ref, out 키워드를 사용하지 않는 한 기본적으루 모두 복사 후 새변수에 할당입니다.

다만 그 대상이 argument의 타입에 따라

  • 값형식일 경우 값복사 후 새 변수에 할당
  • 참조형식일 경우 참조복사 후 새 변수에 할당

이지요.

void Main()
{
	var a = new A(1);
	Console.WriteLine($"a hash :{a.GetHashCode()}");
	
	var aa = Set100(a);
	Console.WriteLine($"Set100 aa hash :{aa.GetHashCode()}");
	
	Console.WriteLine($"a:{a.Number}");
	Console.WriteLine($"aa:{aa.Number}");		
}

private A Set100(A aa) // 요기서 a 의 참조를 복사해 aa 변수에 할당.
{
   // 밖에서 전달한 a 와 파라미터 aa 는 같은 참조를 지닌 다른 변수입니다.

	Console.WriteLine($"aa hash :{aa.GetHashCode()}");

	aa = new A(100); // 이건 밖에서 전달한 a 가 아닌 aa 변수에 새 객체 할당.
	return aa;
}

class A 
{
	public int Number { get; set; }

    public A(int number) => this.Number = number;
}

C# 을 그냥 책으루 공부하다보믄 메서드 인자 전달 방식에 대한 설명이
보통 그냥 값형식은 값복사, 참조형식은 참조전달… 이라고만 되어 있어서 이런 혼란이 오는 거 같아요.

제 생각에… 요걸 혼란스럽지 않게 표현한다면

메서드에 인자 전달은 ref, out 키워드 가 없을 때는 그냥 무조건 복사해서 새 변수에 담는다.
다만 인자의 타입에 따라 복사 대상이 달라질 뿐이다.

이렇게 이해하는 게 젤 덜 혼란스러울 거 같습니다.

2개의 좋아요

아…너무 좋은 설명 감사합니다.
저는 힙이라고 생각했는데 이런것까지 예상해서 답변주셔서 정말 감사드립니다.

1개의 좋아요

네… 좀 더 정확한 표현은 string 자체는 힙에 위치하고 string에서 참조하는 문자 리터럴은 힙에 위치하지 않는다. 가 정확합니다. 위에 이렇게 정확하게 표현하지 못한 것 같습니다.

1개의 좋아요

이미지가 안보여서 테스트로 붙여보았습니다.

1개의 좋아요