Devin.KR

변수와 기본 타입 - 값 형식과 참조 형식

개발자KR 조회 2

이 장에서 배우는 것

앞 장에서 콘솔 앱을 만들고 문장을 출력했다. 이제 동네 카페에서 실제로 쓰는 값을 프로그램에 담는다. 주문 수량, 음료 가격, 우유 사용량, 포장 여부, 메뉴 이름은 모두 데이터지만 같은 방식으로 계산하지는 않는다. 수량에는 정수가 어울리고, 메뉴 이름에는 문자열이 어울린다. 값의 성격에 맞는 타입을 고르면 가능한 연산과 잘못된 대입을 코드에서 구분할 수 있다.

이 장에서는 주문 한 건을 고정된 데이터로 작성한다. 사용자 입력이나 조건 판단은 넣지 않고, 변수에 값을 저장하고 계산한 결과를 출력하는 데 집중한다. 값 형식과 참조 형식의 차이는 변수 사이에 값을 대입할 때 무엇이 복사되는지를 기준으로 살펴본다.

  • int, double, decimal, bool, string을 데이터의 용도에 맞게 선택한다.
  • 변수 선언과 대입을 구분하고 var가 타입을 결정하는 시점을 이해한다.
  • 값 형식과 참조 형식의 대입을 설명하고 string의 변경 불가능한 성질을 구분한다.
  • 암시적 변환과 명시적 변환에서 값이 어떻게 달라지는지 확인한다.
  • null 가능 참조형으로 값이 없는 상태를 표시하고 안전하게 출력한다.

문제 상황

카페 직원이 라테 세 잔을 주문받았다. 한 잔은 4,500원이고 주문 전체에 포장비 300원이 붙는다. 재고 장부에는 컵이 18개 있으며, 라테 한 잔에 들어가는 우유는 0.25리터다. 손님이 별도 요청을 하지 않았으므로 요청 사항은 아직 없다.

이 정보를 모두 문자열로 저장하면 화면에는 쉽게 표시할 수 있다. 그러나 수량에 가격을 곱하거나 재고에서 주문 수량을 빼려면 다시 숫자로 해석해야 한다. 반대로 모든 숫자를 정수로 저장하면 우유 0.25리터를 그대로 표현할 수 없다. 데이터의 의미와 타입을 먼저 연결해야 하는 이유다.

재고 계산에도 구분이 필요하다. 주문 확정 전에는 현재 재고를 유지한 채 예상 재고만 계산하고 싶다. 두 변수에 같은 수를 담고 하나를 바꾸었을 때 다른 하나까지 바뀌는지 알아야 한다. 요청 사항처럼 값이 없는 데이터는 빈 문자열과 같은 뜻으로 처리해도 되는지도 결정해야 한다. 작은 주문표에도 저장, 복사, 계산, 부재의 표현이라는 문제가 함께 들어 있다.

변수와 기본 타입으로 주문 표현하기

선언은 저장할 값의 규칙을 정하는 일이다

변수(variable)는 이름을 통해 값을 읽고 바꿀 수 있는 저장 위치다. 다음 선언에서 int는 타입, quantity는 이름, 3은 처음 저장하는 값이다. 선언과 함께 초기 값을 지정하는 일을 초기화라고 한다. 문장 끝의 세미콜론은 해당 문장이 끝났음을 나타낸다.

int quantity = 3;
quantity = 4;

첫 줄은 변수를 선언하고 초기화한다. 둘째 줄은 이미 있는 변수에 다른 값을 대입한다. 대입 연산자 =는 두 값이 같은지 묻는 기호가 아니다. 오른쪽 식을 계산한 다음 그 결과를 왼쪽 변수에 저장한다. 따라서 quantity = quantity + 1;은 현재 값을 읽어 1을 더한 뒤 같은 변수에 다시 저장하라는 뜻이다.

타입(type)은 저장할 수 있는 값과 허용되는 연산을 정한다. int 변수에는 이 장에서 사용하는 수량과 재고처럼 소수 부분이 없는 정수를 담는다. 문자열을 int에 그대로 대입하면 컴파일러가 잘못된 조합을 알려 준다. 변수 이름에는 quantity, unitPrice처럼 데이터의 의미가 드러나는 말을 쓴다. 이름은 대소문자를 구분하므로 quantity와 Quantity는 서로 다른 이름이다.

카페 데이터에 맞는 다섯 가지 기본 타입
타입표현하는 값카페 예초기 값 예
int32비트 부호 있는 정수주문 수량, 컵 재고3
double이진 부동소수점 수우유 사용량, 측정값0.25
decimal십진 표현에 맞춘 수단가, 결제 금액4500m
bool참 또는 거짓포장 여부true
string문자열메뉴 이름"라테"

숫자는 용도에 따라 고른다

int의 범위는 -2,147,483,648부터 2,147,483,647까지다. 카페 주문 한 건의 수량에는 충분하지만 모든 정수를 담을 수 있는 것은 아니다. 또한 int는 음수도 저장할 수 있다. 수량을 int로 선언했다고 해서 음수 주문을 자동으로 막아 주지는 않는다. 타입이 표현할 수 있는 범위와 업무에서 허용하는 범위는 별개다.

double은 넓은 범위의 실수를 근삿값으로 표현하는 데 쓰인다. 소수점이 붙은 0.25와 같은 숫자 표기는 기본적으로 double이다. 다만 이진 표현을 사용하므로 십진 소수를 모두 정확히 저장하지는 못한다. 0.25는 정확히 표현할 수 있지만 0.1은 그렇지 않다. 화면에 짧게 출력된 값만 보고 내부 계산도 십진수와 똑같다고 판단하면 안 된다.

decimal은 금액처럼 십진 자릿수가 중요한 계산에 자주 사용한다. 숫자 뒤에 m을 붙이면 decimal 값임을 나타낸다. 4500m과 0.1m이 그 예다. decimal도 범위와 정밀도가 정해져 있으므로 모든 나눗셈 결과를 정확하게 저장하는 타입은 아니다. 할인 금액의 원 미만을 어떻게 처리할지는 별도의 업무 규칙이다. 이 장에서는 단가와 포장비가 정수 금액이고 나눗셈도 하지 않으므로 반올림 규칙이 필요하지 않다.

bool에는 true 또는 false를 저장한다. C#에서는 1을 bool 변수에 대입해서 참을 나타내지 않는다. string에는 큰따옴표로 둘러싼 문자열을 저장한다. "3"은 문자열이고 3은 정수이므로 두 값의 의미와 가능한 연산이 다르다. 같은 모양으로 출력되더라도 타입까지 같아지는 것은 아니다.

var는 타입 결정을 컴파일러에 맡긴다

var quantity = 3;
var packagingFee = 300m;
var menuName = "라테";

var를 사용하면 컴파일러가 초기 값으로 지역 변수의 타입을 추론한다. 위 변수의 타입은 차례대로 int, decimal, string이다. 프로그램이 실행되는 동안 값에 따라 타입이 바뀌는 방식이 아니다. var quantity = 3; 뒤에 quantity = "셋";을 쓰면 int 변수에 문자열을 넣는 것이므로 컴파일되지 않는다.

var 선언에는 타입을 판단할 수 있는 초기 값이 필요하다. var quantity;처럼 이름만 쓰거나 var note = null;처럼 null만 주면 타입을 정할 수 없다. 선언에서 데이터의 종류를 강조하고 싶으면 타입 이름을 쓰고, 초기 값만으로 타입이 명확하면 var를 선택할 수 있다. 완성 코드에서는 포장비를 300m으로 초기화하여 var가 decimal을 추론하는 모습을 확인한다.

값 형식과 참조 형식은 무엇을 복사하는가

값 형식은 대입할 때 값을 복사한다

값 형식(value type)의 변수는 해당 타입의 값을 직접 가진다. 이 장의 int, double, decimal, bool은 값 형식이다. 한 변수의 값을 다른 변수에 대입하면 대입 시점의 값이 복사된다. 이후 한 변수에 다른 값을 저장해도 먼저 복사한 변수의 값까지 함께 바뀌지는 않는다.

int stock = 18;
int plannedStock = stock;
plannedStock = plannedStock - 3;

둘째 줄이 끝나면 두 변수의 값은 모두 18이다. 셋째 줄은 plannedStock의 값을 15로 바꾼다. stock의 값은 여전히 18이다. 두 변수가 같은 숫자를 가진다는 사실과 하나의 저장 위치를 공유한다는 주장은 다르다. 이 예에서는 현재 재고를 보존하면서 주문 후 예상 재고를 계산할 수 있다.

int 값을 복사한 뒤 예상 재고를 바꾸어도 원래 재고는 18로 유지된다

그림은 대입 전후의 논리적인 상태를 보여 준다. 값 형식을 설명할 때 특정 메모리 영역에만 저장된다고 외우면 이후 다른 상황을 이해하기 어렵다. 지금 필요한 기준은 저장 장소의 이름이 아니라 대입 시 무엇이 복사되고 어느 변수의 값이 바뀌는가다.

참조 형식은 대입할 때 참조를 복사한다

참조 형식(reference type)의 변수에는 객체를 가리키는 참조가 들어간다. 객체 자체가 아니라 그 참조를 다른 변수로 복사할 수 있으므로, 여러 변수가 같은 객체를 가리킬 수 있다. string이 참조 형식에 속한다.

string menuName = "라테";
string displayName = menuName;
displayName = "아이스 " + displayName;

둘째 줄이 끝나면 menuName과 displayName은 같은 문자열 객체를 가리킨다. 셋째 줄의 +는 문자열을 연결하여 "아이스 라테"라는 결과를 만든다. 대입은 displayName이 그 결과 문자열을 가리키도록 바꾼다. menuName은 계속 "라테"를 가리킨다.

여기에는 두 가지 구분이 필요하다. 참조 변수에 다른 참조를 대입하는 일과, 참조가 가리키는 객체의 내용을 수정하는 일은 서로 다르다. 더구나 string은 생성된 문자열의 내용을 바꿀 수 없는 타입이다. 이를 불변(immutable)이라고 한다. 문자열 연결은 원래 "라테"의 내용을 고치는 작업이 아니다. 이 예에서 다른 변수의 출력이 유지되는 현상을 모든 참조 형식의 객체 수정에도 그대로 적용해서는 안 된다.

문자열 변수를 재대입하면 그 변수의 참조만 바뀌고 다른 변수는 기존 문자열을 계속 가리킨다

string 변수를 다시 대입할 수 있다는 사실과 문자열 객체가 불변이라는 사실은 모순이 아니다. 변수에는 다른 문자열을 가리키는 참조를 저장할 수 있지만 이미 존재하는 문자열의 내용 자체를 고치지는 못한다. 이 차이를 이해하면 문자열 연결을 재고 숫자의 변경과 구분해서 읽을 수 있다.

형 변환과 값이 없는 상태 다루기

변환이 자동으로 되는 경우와 표시해야 하는 경우

형 변환은 한 타입의 값을 다른 타입의 값으로 바꾸는 일이다. int에서 double로의 변환은 별도의 변환 표시 없이 가능하다. 모든 int 값은 double로 정확히 표현할 수 있으므로 이 조합에서는 정수 값이 유지된다. 이렇게 컴파일러가 허용하는 변환을 암시적 변환이라고 한다. 다만 다른 숫자 타입 조합까지 모두 정밀도를 보존한다고 일반화하면 안 된다.

int quantity = 3;
double quantityForMeasurement = quantity;

double에서 int로 바꾸려면 (int)처럼 목적 타입을 표시하는 명시적 변환이 필요하다. 이러한 표기를 캐스트(cast)라고 한다. int 범위 안에 있는 유한한 값을 변환할 때 소수 부분은 0을 향해 버려진다. 3.9는 3이 되고 -3.9는 -3이 된다. 반올림이나 항상 더 작은 정수로 내리는 계산과는 다르다.

double estimatedCups = 3.9;
int wholeCups = (int)estimatedCups;

캐스트를 썼다고 입력 범위까지 검사한 것은 아니다. int 범위를 벗어나는 값이나 숫자로 취급할 수 없는 특별한 double 값을 이 방식으로 처리할 때는 추가 고려가 필요하다. 이 장의 예제는 범위 안의 유한한 값만 사용한다. 완성 코드에서는 0.75리터를 0.25리터로 나누어 세 잔을 계산하므로 소수 부분 손실도 없다.

decimal과 double 사이에는 암시적 변환이 없다. 두 타입을 섞어 계산하려면 표현 방식을 선택하고 필요한 변환을 표시해야 한다. 처음부터 금액인 값은 decimal로 선언하는 편이 의도를 드러낸다. double로 계산한 결과를 마지막에 decimal로 바꾼다고 앞선 계산의 정확성이 보장되지는 않는다.

문자열을 숫자로 해석하는 것은 숫자 타입 사이의 캐스트와 다른 작업이다. (int)"3"은 사용할 수 없다. 반대로 숫자를 출력용 문자열로 만들 때는 ToString을 사용할 수 있다. 뒤의 완성 코드에서 ToString에 지정하는 서식은 화면에 표시할 자릿수를 정하며, 원래 변수에 저장된 숫자를 바꾸지는 않는다.

나눗셈은 계산 시점의 타입을 따른다

double cupsPerOrder = 7 / 2;
double preciseCupsPerOrder = (double)7 / 2;

첫 줄에서는 두 피연산자가 int이므로 정수 나눗셈이 먼저 수행된다. 결과 3을 double로 바꾸어 저장해도 버려진 소수 부분은 돌아오지 않는다. 둘째 줄에서는 7을 먼저 double로 바꾸므로 나눗셈 결과가 3.5다. 결과를 저장하는 변수의 타입만 볼 것이 아니라 연산이 수행되는 순간 각 값의 타입을 확인해야 한다.

null은 빈 문자열과 다른 상태다

null은 참조가 객체를 가리키지 않는 상태를 나타낸다. 손님이 요청 사항을 남기지 않았다면 string? note = null;처럼 표현할 수 있다. 빈 문자열 ""은 길이가 0인 문자열 객체의 값이다. null과 빈 문자열을 업무에서 같은 뜻으로 취급할지는 별도로 정해야 한다. 여기서는 null을 요청 사항이 제공되지 않았다는 뜻으로 사용한다.

null 가능 참조형(nullable reference type)을 사용하면 string은 null이 아니어야 한다는 의도를, string?은 null일 수 있다는 의도를 표현한다. 컴파일러는 코드의 흐름을 분석하여 null일 수 있는 값의 사용에 경고를 낸다. 물음표가 실행 중의 별도 문자열 타입이나 자동 검사 장치를 만드는 것은 아니다. 완성 코드에서는 #nullable enable로 이 분석을 명시적으로 켠다.

string? note = null;
string printableNote = note ?? "요청 없음";

??는 왼쪽 값이 null일 때 오른쪽 값을 사용한다. note가 실제 문자열을 가리키면 그 문자열을 사용하고, null이면 "요청 없음"을 사용한다. 따라서 printableNote에는 null이 아닌 문자열이 들어간다. note가 ""이면 null이 아니므로 빈 문자열을 그대로 사용한다. 이 차이는 빈 요청 사항까지 기본 문구로 바꾸려는 요구가 생겼을 때 다시 살펴봐야 한다.

완성 코드

앞 장에서 만든 .NET 10 콘솔 프로젝트의 Program.cs를 다음 내용으로 바꾼다. 이 파일은 최상위 문으로 작성되므로 별도의 시작 메서드를 선언하지 않는다. 우유 재고는 주문에 사용할 수 있는 0.75리터로 가정한다. 컵 재고는 현재 값과 예상 값을 따로 보관한다.

숫자의 소수점과 자릿수 구분은 실행 환경의 문화권 설정에 따라 달라질 수 있다. 이 예제는 CultureInfo.InvariantCulture를 출력에 사용하여 macOS와 Linux에서 같은 숫자 모양을 얻는다. N0은 소수 자릿수 없이 그룹 구분을 넣고, F2는 소수점 아래 두 자리를 표시한다.

#nullable enable
using System;
using System.Globalization;

CultureInfo outputCulture = CultureInfo.InvariantCulture;

int quantity = 3;
int stock = 18;
int plannedStock = stock;
plannedStock = plannedStock - quantity;

decimal unitPrice = 4500m;
var packagingFee = 300m;
decimal subtotal = unitPrice * quantity;
decimal total = subtotal + packagingFee;

double milkPerCup = 0.25;
double milkNeeded = milkPerCup * quantity;
double availableMilk = 0.75;
int possibleCups = (int)(availableMilk / milkPerCup);

bool isTakeout = true;
string menuName = "라테";
string displayName = menuName;
displayName = "아이스 " + displayName;

string? note = null;
string printableNote = note ?? "요청 없음";

Console.WriteLine($"기본 메뉴: {menuName}");
Console.WriteLine($"주문 메뉴: {displayName}");
Console.WriteLine($"주문 수량: {quantity.ToString(outputCulture)}잔");
Console.WriteLine($"현재 컵 재고: {stock.ToString(outputCulture)}개");
Console.WriteLine($"주문 후 예상 컵 재고: {plannedStock.ToString(outputCulture)}개");
Console.WriteLine($"음료 금액: {subtotal.ToString("N0", outputCulture)}원");
Console.WriteLine($"포장비: {packagingFee.ToString("N0", outputCulture)}원");
Console.WriteLine($"결제 금액: {total.ToString("N0", outputCulture)}원");
Console.WriteLine($"필요한 우유: {milkNeeded.ToString("F2", outputCulture)}L");
Console.WriteLine($"우유 기준 제조 가능 수량: {possibleCups.ToString(outputCulture)}잔");
Console.WriteLine($"포장 여부: {isTakeout}");
Console.WriteLine($"요청 사항: {printableNote}");

줄별 해설

첫 줄의 #nullable enable은 이 파일에서 null 가능 참조형 분석을 활성화한다. 이어지는 두 using 문은 Console과 CultureInfo를 짧은 이름으로 사용할 수 있게 한다. outputCulture 선언은 뒤의 숫자 출력에서 반복해서 사용할 문화권 정보를 이름에 담는다.

quantity와 stock은 각각 주문 수량과 현재 컵 재고다. plannedStock = stock은 18을 복사한다. 다음 줄은 복사한 값에서 주문 수량을 빼고 결과 15를 plannedStock에 저장한다. 현재 재고를 나타내는 stock에는 새 값을 대입하지 않았으므로 18이 유지된다.

unitPrice의 m은 4500을 decimal로 지정한다. packagingFee는 var로 선언했지만 초기 값 300m으로 인해 타입이 decimal로 정해진다. subtotal 계산에서는 int인 quantity가 decimal로 암시적으로 변환되어 곱셈에 참여한다. 따라서 4,500원에 세 잔을 곱한 13,500원이 decimal 값으로 저장된다. total에는 주문 한 건의 포장비 300원을 더한 13,800원을 저장한다.

milkPerCup과 milkNeeded는 리터 단위의 사용량이다. 곱셈에서 quantity는 double로 암시적으로 변환된다. 이 예제에서 0.25와 계산 결과 0.75는 이진 표현으로 정확히 나타낼 수 있다. possibleCups 선언은 먼저 나눗셈을 계산한 후 바깥의 (int)를 적용한다. 괄호는 변환 대상이 availableMilk 하나가 아니라 나눗셈 결과 전체임을 보여 준다.

isTakeout은 포장 여부를 저장한다. 이 프로그램은 포장 주문 한 건만 표현하므로 포장비를 더할지 판단하는 코드는 없다. bool 값과 포장비를 서로 연동하는 동작은 아직 작성하지 않았다. isTakeout만 false로 바꿔도 금액 계산이 자동으로 달라지는 것은 아니다.

menuName을 displayName에 대입하면 문자열 참조가 복사된다. 다음 줄은 문자열 연결 결과를 displayName에 다시 대입한다. 따라서 기본 메뉴와 주문 메뉴는 각각 "라테", "아이스 라테"로 출력된다. note는 null을 허용하고, printableNote는 ??로 선택한 출력용 문자열을 저장한다.

마지막 출력 줄들에서 $"..."는 문자열 보간(string interpolation)이다. 중괄호 안의 식을 평가하여 결과를 문자열에 넣는다. 숫자는 지정된 문화권과 서식으로 변환한 뒤 삽입한다. ToString("N0", outputCulture)에 있는 큰따옴표는 중괄호 안의 식에 속하므로 그대로 작성한다. bool을 기본 문자열로 출력하면 코드의 true와 달리 첫 글자가 대문자인 True가 나온다.

실행 결과

프로젝트 파일이 있는 디렉터리에서 다음 명령을 실행한다. 외부 입력, 현재 시각, 임의의 수를 사용하지 않으므로 같은 코드의 프로그램 출력은 일정하다.

dotnet run
기본 메뉴: 라테
주문 메뉴: 아이스 라테
주문 수량: 3잔
현재 컵 재고: 18개
주문 후 예상 컵 재고: 15개
음료 금액: 13,500원
포장비: 300원
결제 금액: 13,800원
필요한 우유: 0.75L
우유 기준 제조 가능 수량: 3잔
포장 여부: True
요청 사항: 요청 없음

실무에서 자주 틀리는 것

다음 코드는 오류를 비교하기 위한 부분 예제다. 잘못된 예제는 컴파일 오류나 경고를 의도적으로 포함할 수 있으므로 완성 코드에 덧붙여 실행하지 않는다.

금액을 선언하면서 m을 빠뜨린다

잘못된 코드는 다음과 같다. 4500.0은 double이므로 decimal 변수에 암시적으로 대입할 수 없다.

decimal unitPrice = 4500.0;

금액의 초기 값 자체를 decimal로 지정하면 의도가 분명해진다.

decimal unitPrice = 4500.0m;

소수점 없는 정수 4500은 decimal로 암시적 변환이 가능하다. 따라서 모든 decimal 대입에 m이 필수인 것은 아니다. 여기서는 금액의 타입을 선언과 초기 값 양쪽에서 드러내기 위해 m을 사용한다.

결과 변수가 double이면 소수 부분도 남는다고 생각한다

컵 일곱 개를 두 주문에 나눈 평균이 필요할 때 다음 코드는 의도한 값을 저장하지 않는다.

int cups = 7;
int orders = 2;
double average = cups / orders;

정수 나눗셈 결과인 3이 먼저 만들어지기 때문이다. 연산 전에 한쪽 값을 double로 변환한다.

int cups = 7;
int orders = 2;
double average = (double)cups / orders;

고친 코드의 average는 3.5다. (double)(cups / orders)처럼 나눗셈 전체를 나중에 변환하면 이미 소수 부분이 버려졌으므로 목적을 달성하지 못한다.

var로 선언한 변수의 타입을 바꾸려 한다

잘못된 코드는 수량을 출력용 문구로 바꾸려고 같은 변수에 문자열을 저장한다.

var quantity = 3;
quantity = "3잔";

quantity는 이미 int로 결정되었다. 계산용 수량과 출력용 문자열에 서로 다른 이름을 준다.

var quantity = 3;
string quantityText = quantity.ToString(
    System.Globalization.CultureInfo.InvariantCulture) + "잔";

고친 코드에서는 quantity로 계속 수량 계산을 할 수 있고 quantityText로 표시 문구를 사용할 수 있다. 출력 형식을 만들었다는 이유로 원래 데이터의 역할까지 바꿀 필요는 없다.

null일 수 있는 문자열에서 곧바로 길이를 읽는다

다음 코드는 null 가능 참조형 분석이 켜져 있으면 경고가 발생한다. null에는 길이를 읽을 문자열 객체가 없으므로 그대로 실행하면 예외도 발생한다.

#nullable enable
string? note = null;
int noteLength = note.Length;

요청이 없을 때 길이를 0으로 취급하기로 했다면 먼저 null을 빈 문자열로 대체한다.

#nullable enable
string? note = null;
string noteForCounting = note ?? "";
int noteLength = noteForCounting.Length;

Length는 문자열의 길이를 읽는 속성이다. 이 코드에서는 빈 문자열의 길이가 0이다. 이 선택은 글자 수 계산에서 두 상태를 같게 취급한다는 뜻이며, 원래 note에 저장된 null까지 빈 문자열로 바꾸지는 않는다. 참고로 Length는 UTF-16 코드 단위 수이므로 모든 문자에서 사람이 세는 글자 수와 일치하지는 않는다.

한눈에 보기

선언, 복사, 변환, null 처리에서 확인할 기준
항목핵심 규칙코드에서 확인할 점
값 형식대입하면 값이 복사된다plannedStock 변경은 stock을 바꾸지 않는다
참조 형식대입하면 참조가 복사된다참조 재대입과 객체 내용 변경을 구분한다
string참조 형식이며 문자열 내용은 불변이다연결 결과를 변수에 다시 대입한다
var초기 값으로 타입을 추론한다선언 뒤에도 타입은 유지된다
숫자 변환조합에 따라 명시적 변환이 필요하다범위와 소수 부분 손실을 확인한다
정수 나눗셈정수끼리 나누면 정수 결과를 얻는다소수가 필요하면 나누기 전에 변환한다
string?null일 수 있음을 표시한다문자열을 사용하기 전에 부재를 처리한다
??왼쪽이 null이면 오른쪽을 사용한다빈 문자열에는 대체 값이 적용되지 않는다

타입 선택은 계산 규칙의 출발점이다. 금액은 decimal, 측정값은 double, 개수는 int처럼 데이터의 의미를 먼저 정하면 코드가 읽기 쉬워진다. 이후 대입이 값의 복사인지 참조의 복사인지, 변환 과정에서 정보가 없어지는지, 값이 없는 경우가 가능한지를 차례로 확인한다.

연습 문제

  1. 완성 코드에서 주문 수량을 2로, 단가를 4800m으로 바꾼다. 다른 초기 값은 유지한다. 주문 후 예상 컵 재고, 음료 금액, 결제 금액, 필요한 우유를 구한다. 우유 기준 제조 가능 수량도 달라지는지 설명한다.
  2. int original = 18; int copied = original; copied = 11;을 실행한 뒤 두 변수의 값을 적는다. 이어서 string first = "라테"; string second = first; second = "모카";를 실행한 뒤 두 문자열 변수의 값을 적고, 두 대입에서 복사한 대상의 차이를 설명한다.
  3. double average = 9 / 4;의 저장 결과를 적고, 2.25를 저장하도록 오른쪽 식을 고친다. 이어서 (int)2.9와 (int)(-2.9)의 결과를 적는다.
  4. 완성 코드의 note를 먼저 "덜 뜨겁게"로, 다음에는 ""로 바꾸었을 때 printableNote에 저장되는 값을 적는다. note가 null인 원래 상태와 비교하여 ??가 어떤 상태를 대체하는지 설명한다.

정답과 해설

  1. 예상 컵 재고는 16개다. 음료 금액은 4,800 × 2로 9,600원이며 포장비 300원을 더한 결제 금액은 9,900원이다. 필요한 우유는 0.25 × 2로 0.50L다. 우유 기준 제조 가능 수량은 여전히 3잔이다. 이 값은 주문 수량이 아니라 availableMilk와 milkPerCup으로 계산하기 때문이다.

  2. original은 18, copied는 11이다. int 대입은 정수 값을 복사하므로 copied의 이후 변경이 original에 영향을 주지 않는다. first는 "라테", second는 "모카"다. string 대입은 참조를 복사하지만 second에 다른 참조를 대입해도 first의 참조는 바뀌지 않는다. 이 코드는 기존 문자열 객체의 내용을 수정하지 않는다.

  3. 원래 average에는 2가 double 값으로 저장된다. 오른쪽을 (double)9 / 4 또는 9.0 / 4로 고치면 2.25가 저장된다. (int)2.9는 2이고 (int)(-2.9)는 -2다. 두 변환 모두 소수 부분을 0을 향해 버리므로 반올림 결과로 해석하지 않는다.

  4. note가 "덜 뜨겁게"이면 printableNote에도 같은 문자열이 저장된다. note가 ""이면 printableNote도 빈 문자열이다. 원래처럼 null일 때만 "요청 없음"이 저장된다. ??는 내용이 비었는지 판단하는 연산자가 아니라 null을 대체하는 연산자다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.