VB.NET 이든 C# 이든 업무 코드에서 제일 자주 하는 일 중 하나가 문자열을 잘라 배열로 만들고, 다시 배열을 하나의 문자열로 붙이는 일입니다. CSV 한 줄을 쪼개고, 체크된 항목들을 쉼표로 이어 SQL IN (...) 절을 만들고, 바이트 배열을 로그에 찍는 것까지 전부 여기에 해당합니다. 두 언어로 같이 개발하다 보면 "VB 로는 이거 어떻게 쓰더라" 하고 문법이 헷갈릴 때가 많아서, 같은 동작을 C# 과 VB 로 나란히 적어 둡니다.
원본 코드는 C# 으로 먼저 작성하고 VB 로 변환한 것이라 VB 쪽에 변환기 특유의 흔적(.[Select] 같은 대괄호 이스케이프)이 남아 있습니다. [Select] 는 Select 가 VB 예약어라서 붙은 것인데, 메서드 호출 위치에서는 대괄호 없이 .Select(...) 로 써도 문제없습니다.
기본 선언
int[] myArray = new int[5] { 10, 15, 20, 25, 30 };
string myString = "Hello World!";
Dim myArray As Integer() = New Integer(4) {10, 15, 20, 25, 30}
Dim myString As String = "Hello World!"
C# 의 new int[5] 는 "요소 5개", VB 의 New Integer(4) 는 "마지막 인덱스 4"라는 뜻입니다. 둘 다 0~4 다섯 칸인데 숫자가 하나 차이 나는 게 처음엔 꼭 헷갈립니다.
문자열을 배열로: Split 과 Regex.Split
공백 하나로 나누는 가장 단순한 경우입니다.
string input = "Coding is fun!";
char[] delimiter = new char[] { ' ' };
string[] output = input.Split(delimiter);
// Output: ["Coding", "is", "fun!"]
Dim input As String = "Coding is fun!"
Dim delimiter As Char() = New Char() {" "c}
Dim output As String() = input.Split(delimiter)
'// Output: ["Coding", "is", "fun!"]
VB 에서 문자(Char) 리터럴은 " "c 처럼 뒤에 c 를 붙입니다. 빼먹으면 String 이 되어 Option Strict On 에서 오류가 납니다. 그리고 공백이 두 칸 연속이면 빈 문자열 요소가 생기니, 사용자 입력을 쪼갤 때는 StringSplitOptions.RemoveEmptyEntries 를 같이 주는 습관을 들이는 게 좋습니다.
구분자가 여러 종류 섞여 있으면 정규식으로 나눕니다. 엑셀에서 붙여 넣은 값에 쉼표와 세미콜론이 섞여 들어오는 경우에 유용합니다.
using System.Text.RegularExpressions;
string input = "C#;Java,Python";
string pattern = @"[,;]+";
string[] output = Regex.Split(input, pattern);
// Output: ["C#", "Java", "Python"]
Using System.Text.RegularExpressions
End Using
Dim input As String = "C#;Java,Python"
Dim pattern As String = "[,;]+"
Dim output As String() = Regex.Split(input, pattern)
'// Output: ["C#", "Java", "Python"]
주의: VB 쪽의 Using ... End Using 두 줄은 C# 의 using 지시문을 변환기가 잘못 옮긴 것입니다. VB 에서 Using 은 IDisposable 객체를 다 쓰고 해제하는 블록이라 이렇게 쓰면 컴파일 오류가 납니다. 네임스페이스를 가져올 때는 파일 맨 위에 Imports System.Text.RegularExpressions 로 써야 합니다. 아래 LINQ 예제의 Using System.Linq 도 마찬가지로 Imports System.Linq 가 맞습니다(VB 프로젝트는 대개 기본으로 가져와져 있어서 생략해도 됩니다).
배열을 문자열로: Join 과 Concat
string[] words = new string[] { "C#", "is", "awesome!" };
string separator = " ";
string result = String.Join(separator, words);
// Result: "C# is awesome!"
Dim words As String() = New String() {"C#", "is", "awesome!"}
Dim separator As String = " "
Dim result As String = String.Join(separator, words)
'// Result: "C# is awesome!"
String.Join 은 구분자를 사이에만 넣어 줍니다. 루프를 돌면서 & "," 를 붙이고 마지막 쉼표를 잘라내는 코드를 아직도 종종 보는데, 이걸로 한 줄이면 됩니다.
숫자 배열처럼 문자열이 아닌 배열은 Select 로 문자열로 바꾼 뒤 Concat 으로 구분자 없이 붙일 수 있습니다.
using System.Linq;
int[] myArray = new int[] { 1, 2, 3, 4, 5 };
string result = string.Concat(myArray.Select(x => x.ToString()));
// Result: "12345"
Using System.Linq
End Using
Dim myArray As Integer() = New Integer() {1, 2, 3, 4, 5}
Dim result As String = String.Concat(myArray.[Select](Function(x) x.ToString()))
'// Result: "12345"
조건에 맞는 것만 골라서 합칠 때는 Where 를 끼웁니다. 아래는 4글자 이하 항목만 쉼표로 잇는 예입니다.
string[] input = new string[] { "C#", "Java", "Python", "JavaScript", "Ruby" };
string result = string.Join(", ", input.Where(x => x.Length <= 4));
// Result: "C#, Java, Ruby"
Dim input As String() = New String() {"C#", "Java", "Python", "JavaScript", "Ruby"}
Dim result As String = String.Join(", ", input.Where(Function(x) x.Length <= 4))
'// Result: "C#, Java, Ruby"
Select 에 인덱스를 받는 오버로드를 쓰면 마지막 요소에만 다른 처리를 할 수도 있습니다. 대문자로 바꾸면서 마지막에는 쉼표 대신 마침표를 붙이는 예제입니다.
string[] input = new string[] { "apple", "banana", "cherry" };
string result = string.Concat(input.Select((x, i) => x.ToUpper() + (i < input.Length - 1 ? ", " : ".")));
// Result: "APPLE, BANANA, CHERRY."
Dim input As String() = New String() {"apple", "banana", "cherry"}
Dim result As String = String.Concat(input.[Select](Function(x, i) x.ToUpper() & (If(i < input.Length - 1, ", ", "."))))
'// Result: "APPLE, BANANA, CHERRY."
C# 의 삼항 연산자 ? : 가 VB 에서는 If(조건, 참, 거짓) 입니다. 옛날 IIf() 함수와 달리 If() 는 선택된 쪽만 평가하므로 이쪽을 쓰는 게 맞습니다.
바이트 배열과 객체 배열
바이트 배열은 BitConverter.ToString 으로 16진수 문자열로 바꿀 수 있습니다. 시리얼 통신이나 소켓으로 주고받은 패킷을 로그에 찍을 때 제가 제일 많이 쓰는 방법입니다.
byte[] byteArray = new byte[] { 72, 101, 108, 108, 111 };
string result = BitConverter.ToString(byteArray);
// Result: "48-65-6C-6C-6F"
Dim byteArray As Byte() = New Byte() {72, 101, 108, 108, 111}
Dim result As String = BitConverter.ToString(byteArray)
'// Result: "48-65-6C-6C-6F"
하이픈 없이 붙이고 싶으면 결과에 .Replace("-", "") 를 붙이면 됩니다. 바이트를 글자로 해석하고 싶다면(72, 101… 은 "Hello") 이건 Encoding.ASCII.GetString 같은 인코딩 클래스의 몫입니다.
직접 만든 클래스의 배열을 Join 하면 기본적으로 클래스 이름만 찍힙니다. ToString 을 재정의해 두면 Join 이나 디버거, ListBox 표시에서 원하는 모양으로 나옵니다.
public class MyClass
{
public int Id;
public string Name;
public override string ToString()
{
return $"Id: {Id}, Name: {Name}";
}
}
Public Class [MyClass]
Public Id As Integer
Public Name As String
Public Overrides Function ToString() As String
Return $"Id: {Id}, Name: {Name}"
End Function
End Class
VB 에서 MyClass 는 키워드라서 클래스 이름으로 쓰려면 [MyClass] 처럼 대괄호로 감싸야 합니다. 문자열 보간 $"..." 은 VB 2015(VS2015) 이상에서 됩니다.
덤으로 배열 다루기
변환과 직접 관련은 없지만 같이 자주 쓰는 배열 조작들입니다. 배열 크기를 늘려서 요소를 하나 추가하는 경우:
string[] planets = new string[] { "Mercury", "Venus", "Earth" };
Array.Resize(ref planets, planets.Length + 1);
planets[planets.Length - 1] = "Mars";
// Planets: ["Mercury", "Venus", "Earth", "Mars"]
Dim planets As String() = New String() {"Mercury", "Venus", "Earth"}
Array.Resize(planets, planets.Length + 1)
planets(planets.Length - 1) = "Mars"
'// Planets: ["Mercury", "Venus", "Earth", "Mars"]
string[] fruits = new string[] { "apple", "banana" };
Array.Resize(ref fruits, fruits.Length + 1);
fruits[fruits.Length - 1] = "cherry";
// Fruits: ["apple", "banana", "cherry"]
Dim fruits As String() = New String() {"apple", "banana"}
Array.Resize(fruits, fruits.Length + 1)
fruits(fruits.Length - 1) = "cherry"
'// Fruits: ["apple", "banana", "cherry"]
C# 은 ref 를 명시해야 하지만 VB 는 매개변수가 ByRef 로 선언되어 있으면 그냥 넘겨도 됩니다. 다만 Array.Resize 는 매번 새 배열을 만들어 복사하므로, 루프 안에서 하나씩 추가하는 용도라면 List(Of String) 을 쓰고 마지막에 .ToArray() 하는 쪽이 낫습니다.
조건에 맞는 요소만 새 배열로 뽑기, 정렬, 첫 번째로 맞는 요소 찾기:
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
int[] evenNumbers = numbers.Where(x => x % 2 == 0).ToArray();
// Even numbers: [2, 4]
Dim numbers As Integer() = New Integer() {1, 2, 3, 4, 5}
Dim evenNumbers As Integer() = numbers.Where(Function(x) x Mod 2 = 0).ToArray()
'// Even numbers: [2, 4]
string[] names = new string[] { "Alice", "Bob", "Eve", "David", "Carol" };
Array.Sort(names);
// Sorted names: ["Alice", "Bob", "Carol", "David", "Eve"]
Dim names As String() = New String() {"Alice", "Bob", "Eve", "David", "Carol"}
Array.Sort(names)
'// Sorted names: ["Alice", "Bob", "Carol", "David", "Eve"]
int[] numbers = new int[] { 1, 3, 5, 2, 4 };
int firstEvenNumber = Array.Find(numbers, x => x % 2 == 0);
// First even number: 2
Dim numbers As Integer() = New Integer() {1, 3, 5, 2, 4}
Dim firstEvenNumber As Integer = Array.Find(numbers, Function(x) x Mod 2 = 0)
'// First even number: 2
Array.Find 는 못 찾으면 예외가 아니라 형식의 기본값(Integer 면 0, String 이면 Nothing)을 돌려줍니다. 0 이 실제 값일 수 있는 데이터라면 Array.FindIndex 로 -1 여부를 확인하는 게 안전합니다.
날짜를 문자열로
마지막으로 DateTime 을 서식 문자열로 바꾸는 예제입니다.
DateTime dt = new DateTime(2021, 12, 31, 18, 30, 0);
string result_short = dt.ToString("d");
// Result: "12/31/2021"
string result_na = dt.ToString("F");
// Result: "Friday, December 31, 2021 6:30:00 PM"
Dim dt As DateTime = New DateTime(2021, 12, 31, 18, 30, 0)
Dim result_short As String = dt.ToString("d")
'// Result: "12/31/2021"
Dim result_na As String = dt.ToString("F")
'// Result: "Friday, December 31, 2021 6:30:00 PM"
주석의 결과는 영어(en-US) 환경 기준입니다. "d", "F" 같은 표준 서식은 현재 문화권을 따르기 때문에 한국어 윈도우에서 돌리면 2021-12-31, 2021년 12월 31일 금요일 오후 6:30:00 처럼 나옵니다. 파일명이나 DB 저장용처럼 환경과 상관없이 같은 모양이 나와야 할 때는 dt.ToString("yyyy-MM-dd HH:mm:ss") 처럼 사용자 지정 서식을 쓰세요. 서버와 개발 PC의 지역 설정이 달라서 날짜 문자열이 깨지는 문제는 생각보다 자주 겪습니다.