[iOS 2주차] Swift: 메서드 체이닝 / 비트 연산 / .filter() / .enumerated() / .allSatisfy() / zip() / pow()

DoyleHWorks·2024년 10월 31일
0

오늘은 여러가지 알고리즘 문제를 풀면서 몰랐던 메서드나 사고방식을 많이 경험할 수 있었다. 각 알고리즘에서 어떤 메서드나 방법을 활용했는지 간략하게 기록해두었고, 오늘 배운 것 중에 정리해두면 좋겠다고 생각한 것은 ChatGPT를 이용해 정리해놓았다 :)


오늘 푼 알고리즘들


// ChatGPT 4o prompts:
메서드 체이닝에 대해서 설명해볼래?

메서드 체이닝

메서드 체이닝(Method Chaining)은 여러 메서드를 연속해서 호출하여 하나의 구문으로 연결하는 프로그래밍 기법이다. Swift뿐만 아니라 JavaScript, Python, Kotlin 같은 언어에서도 자주 사용되며, 코드의 가독성과 간결성을 높여준다.

메서드 체이닝의 원리

메서드 체이닝은 각 메서드가 특정 값을 반환하고, 이 반환된 값에 대해 다시 메서드를 호출할 수 있는 경우에 가능해진다. 예를 들어, Swift의 배열이나 문자열 메서드는 변환된 새로운 배열이나 문자열을 반환하므로, 반환된 값에 대해 추가 메서드를 호출할 수 있다.

Swift에서 메서드 체이닝 예시

let numbers = [1, 2, 3, 4, 5, 6]
let result = numbers
    .filter { $0 % 2 == 0 }  // 짝수만 필터링
    .map { $0 * $0 }          // 제곱으로 변환
    .reduce(0, +)             // 모든 요소의 합을 계산
print(result)  // 4 + 16 + 36 = 56
let phrase = " Hello, World! "
let result = phrase
    .trimmingCharacters(in: .whitespaces)          // 문자열 앞뒤 공백 제거
    .replacingOccurrences(of: "World", with: "Swift") // "World"를 "Swift"로 대체
    .uppercased()                                  // 모든 문자를 대문자로 변환
print(result)  // "HELLO, SWIFT!"

메서드 체이닝의 장점

  1. 가독성: 각 변환 단계가 연속으로 연결되므로 코드의 흐름이 자연스럽고 직관적이다.
  2. 간결함: 중간 변수 선언이 필요 없으므로 코드 길이가 짧아지고, 불필요한 변수 선언을 줄일 수 있다.
  3. 불변성 유지: 체이닝을 통해 새 값을 반환하여 가공하므로 원본 데이터를 변경하지 않는 방식으로 설계할 수 있다. 이는 함수형 프로그래밍의 주요 개념 중 하나로, 데이터를 안전하게 다룰 수 있게 한다.

메서드 체이닝의 한계

메서드 체이닝은 가독성을 높이지만, 너무 많은 체이닝이 있거나, 각 체인 구문이 복잡할 경우 오히려 가독성이 떨어질 수 있다. 이런 경우 중간에 변수를 사용해 각 단계를 명확히 구분하는 것이 좋다.


// ChatGPT 4o prompts:

  • 비트 연산과 swift의 비트 연산 방법을 설명해줘
  • 비트 시프트는 2배 단위로 숫자가 변화되는거야?
  • 2배로 변화되는 건 예외적인 경우 없이 안심하고 써도 되는거야?

비트 연산

비트 연산(bitwise operations)은 정수형 데이터의 이진 표현을 기준으로 비트 단위로 수행되는 연산이다. 이러한 연산은 일반적으로 컴퓨터가 데이터를 매우 빠르게 조작할 수 있도록 하며, 효율적인 알고리즘을 작성하는 데 중요한 역할을 한다. Swift에서는 아래의 연산자들을 사용하여 비트 단위로 데이터를 조작할 수 있으며, 높은 성능의 저수준 연산을 구현할 때 유용하다. 예를 들어, 특정 비트를 설정하거나 해제할 때 또는 비트 마스킹 기법을 사용할 때 자주 활용된다.

  1. AND (&): 두 비트가 모두 1일 때만 결과가 1이 된다.

    • 예: let result = 5 & 3 (0101 & 00110001, 결과는 1)
  2. OR (|): 두 비트 중 하나라도 1이면 결과가 1이 된다.

    • 예: let result = 5 | 3 (0101 | 00110111, 결과는 7)
  3. XOR (^): 두 비트가 다를 때만 결과가 1이 됩니다.

    • 예: let result = 5 ^ 3 (0101 ^ 00110110, 결과는 6)
  4. NOT (~): 각 비트를 반전합니다(1은 0으로, 0은 1로).

    • 예: let result = ~5 (01011010, 결과는 -6, 2의 보수 표현에 의해 음수로 나타남)
  5. 비트 시프트 (<<, >>):

    • 왼쪽 시프트 (<<): 지정된 수만큼 비트를 왼쪽으로 이동시킵니다. 오른쪽은 0으로 채워집니다.
      • 예: let result = 5 << 1 (0101 << 11010, 결과는 10)
    • 오른쪽 시프트 (>>): 지정된 수만큼 비트를 오른쪽으로 이동시킵니다. 왼쪽은 부호 비트로 채워집니다(부호 있는 정수의 경우).
      • 예: let result = 5 >> 1 (0101 >> 10010, 결과는 2)

※ 비트 시프트는 숫자를 2배 단위로 변환하는 특성이 있다. 이는 정수형 연산에서 곱셈이나 나눗셈보다 더 빠르게 수행된다!

비트 시프트 사용 시 주의할 점

비트 시프트 연산이 숫자를 2배 단위로 변화시키는 것은 일반적으로 안전하게 사용할 수 있지만 몇 가지 예외 상황을 주의해야 한다.

1. 부호 있는 정수(Int)의 경우:

  • 왼쪽 시프트(<<)는 양수의 경우 문제가 없지만, 음수일 때는 예상하지 못한 결과를 초래할 수 있다. 음수를 왼쪽으로 시프트하면 부호 비트가 변할 수 있어 원치 않는 값이 될 수 있다.
  • 오른쪽 시프트(>>)는 음수의 경우 부호 비트를 유지하면서 시프트되기 때문에, 음수일 때 결과는 항상 2로 나누는 결과와 동일하지 않을 수 있다. 음수는 부호를 유지하기 위해 최상위 비트에 1이 채워진다.

2. 정수 오버플로우:

  • 큰 정수를 왼쪽으로 시프트할 때, 비트가 넘쳐서 버려질 수 있다. 이 경우, 결과값은 의도한 것과 다르게 나올 수 있다.
  • 예를 들어, Int 타입에서 가장 큰 값인 Int.max를 왼쪽으로 시프트하면 오버플로우가 발생할 수 있다.

3. 시프트 범위 주의:

  • 시프트 연산은 비트 수보다 더 큰 값으로 시프트할 경우 예상하지 못한 결과를 가져온다. 예를 들어, 32비트 정수에서 << 32는 원래 값을 유지하거나 비트가 사라질 수 있다. Swift에서는 시프트할 비트 수가 음수이거나 타입의 비트 수 이상인 경우 런타임 에러가 발생하지는 않지만, 안전한 범위 내에서만 시프트를 사용하는 것이 좋다.

결론:

  • 양수에 대한 시프트 연산은 왼쪽이든 오른쪽이든 안전하게 사용할 수 있으며, 왼쪽 시프트는 2배, 오른쪽 시프트는 2로 나눈 결과를 줍니다.
  • 음수오버플로우에 주의해야 하며, 부호 있는 정수의 경우 시프트 연산 후 결과를 확인하거나 명시적인 처리를 해주는 것이 좋다.
  • 일반적인 계산에서는 시프트 연산이 빠르고 안전하지만, 특정 상황에서 엣지 케이스를 주의해야 한다.

.filter()

Swift의 filter 메서드는 컬렉션(배열, 집합 등)의 요소들을 특정 조건에 따라 걸러내어 새로운 배열이나 컬렉션을 반환하는 메서드이다. 주어진 클로저의 조건을 만족하는 요소만 포함된 새로운 배열을 만든다.

문법

let filteredArray = array.filter { 조건식 }
  • array: 필터를 적용할 배열 또는 컬렉션.
  • { 조건식 }: 각 요소에 대해 실행될 클로저. 클로저는 요소를 받아 true 또는 false를 반환해야 하며, true인 경우 해당 요소가 결과 배열에 포함된다.

반환값

조건을 만족하는 요소들만 담긴 새로운 배열을 반환한다. 조건을 만족하는 요소가 없다면 빈 배열을 반환한다.

사용 예시

let numbers = [1, 2, 3, 4, 5, 6]
let evenNumbers = numbers.filter { $0 % 2 == 0 } // 배열에서 짝수인 요소만 필터링합니다.
print(evenNumbers) // 출력: [2, 4, 6]

let words = ["apple", "banana", "kiwi", "peach"]
let longWords = words.filter { $0.count > 4 } // 문자열의 길이가 4보다 큰 요소만 필터링합니다.
print(longWords) // 출력: ["apple", "banana"]

장점

  • 간결함: 복잡한 조건문 없이 간단히 원하는 조건에 맞는 요소를 걸러낼 수 있다.
  • 가독성: 코드가 명확하고 직관적이어서 읽기 쉽다.

filter는 Swift에서 조건에 맞는 요소만 빠르게 추출할 때 매우 유용한 메서드이다.


// ChatGPT 4o prompts:

  • enumerated() 를 설명해줘
  • 튜플 안에서 T는 뭘 의미하는거야?

.enumerated()

enumerated()는 Swift의 시퀀스 메서드 중 하나로, 컬렉션의 각 요소에 대해 해당 요소와 그에 대한 인덱스를 튜플로 반환하는 메서드이다. 이 메서드를 사용하면 컬렉션의 요소와 인덱스를 동시에 순회할 수 있다. enumerated()는 배열이나 다른 컬렉션을 순회할 때 인덱스와 요소를 모두 필요로 하는 경우 유용하게 사용할 수 있는 메서드이다.

반환값

enumerated() 메서드는 (offset: Int, element: T) 형태의 튜플 시퀀스를 반환한다.

  • offset: 해당 요소의 인덱스.
  • element: 해당 인덱스에 위치한 요소.

( 제네릭 타입 매개변수: T )

Swift에서 튜플 안의 T는 일반적으로 타입을 의미하는 제네릭 타입 매개변수의 표기이다.

enumerated() 메서드의 반환 타입은 (offset: Int, element: T) 형태의 튜플이다. 여기서 T는 원래 컬렉션의 요소 타입을 나타낸다. 예를 들어, 만약 배열이 [String] 타입이라면 TString이고, [Int] 타입이라면 TInt이다.

사용 예시

let fruits = ["apple", "banana", "cherry"]

// enumerated()를 사용하여 요소와 인덱스를 함께 순회
for (index, fruit) in fruits.enumerated() {
    print("Index \(index): \(fruit)")
}
// 출력:
// Index 0: apple
// Index 1: banana
// Index 2: cherry

// map과 조합하여 인덱스를 활용한 새 배열 생성
let newArray = fruits.enumerated().map { (index, fruit) in
    "\(index + 1). \(fruit)" // 인덱스에 1을 더해 순번으로 표시
}
print(newArray) // ["1. apple", "2. banana", "3. cherry"]

장점

  • 인덱스 참조 가능: 컬렉션 요소를 순회하면서 인덱스를 함께 사용할 수 있어 별도의 인덱스 변수를 만들 필요가 없다.
  • 가독성 향상: 코드가 더 직관적이며, 요소와 인덱스를 동시에 사용하는 작업이 간결해진다.

.allSatisfy()

allSatisfy는 컬렉션의 모든 요소가 특정 조건을 만족하는지 확인하는 함수이다. 조건을 정의하는 클로저를 받아, 모든 요소가 해당 조건을 만족하면 true, 하나라도 만족하지 않으면 false를 반환한다.

let numbers = [1, 2, 3, 4, 5]
print(numbers.allSatisfy { $0 > 0 })  // true, 모든 요소가 0보다 큼

let mixedNumbers = [1, -2, 3, 4, 5]
print(mixedNumbers.allSatisfy { $0 > 0 })  // false, 음수가 있음

let word = "hello"
print(word.allSatisfy { $0.isLowercase })  // true, 모든 문자가 소문자임

let validCharacters = Set("0123456789+-")
let input = "+12345"
print(input.allSatisfy { validCharacters.contains($0) })  // true, 특정 문자 집합에만 속함

zip()

Swift의 zip() 함수는 두 개의 시퀀스를 결합하여 각 시퀀스의 대응하는 요소를 튜플로 묶어주는 함수이다. 두 시퀀스 중 더 짧은 시퀀스의 길이에 맞춰 결합되며, 두 시퀀스의 요소들을 병렬로 처리할 때 유용하다.

문법

let zippedSequence = zip(sequence1, sequence2)
  • sequence1sequence2는 결합할 시퀀스들이다.
  • zippedSequence는 두 시퀀스의 요소가 (element1, element2) 형태의 튜플로 묶인 시퀀스를 반환한다.

사용 예시

let array1 = [1, 2, 3, 4]
let array2 = ["a", "b", "c"]

let zipped = zip(array1, array2)
for (num, letter) in zipped {
    print("\(num): \(letter)")
}
// 출력:
// 1: a
// 2: b
// 3: c

특징

  • 길이 제한: zip()은 두 시퀀스 중 더 짧은 시퀀스의 길이에 맞춰 동작한다. 예시에서 array1의 길이는 4이고, array2의 길이는 3이므로 결과는 3개의 튜플이 반환된다.
  • 비파괴적: zip()은 원래의 시퀀스를 변경하지 않고 새로운 시퀀스를 반환한다.

용도

  • 두 시퀀스를 병렬로 처리할 때 유용하며, 인덱스나 반복문 없이 두 컬렉션의 요소를 동시에 접근할 수 있다. 예를 들어, 두 개의 배열에서 각 요소를 짝지어 합산하거나 비교할 때 사용된다.

pow()

Swift의 pow 함수는 거듭제곱을 계산하는 함수로, 두 개의 Double 값을 매개변수로 받아 첫 번째 값을 두 번째 값으로 거듭제곱한 결과를 반환한다. Foundation 프레임워크에 포함되어 있으며, 소수나 음수 지수도 처리할 수 있다.

import Foundation

// 기본 문법: pow(base, exponent)
// 예시: 양수, 소수, 음수 지수 처리
let positiveExponent = pow(2.0, 3.0)        // 8.0
let fractionalExponent = pow(9.0, 0.5)      // 3.0 (제곱근)
let negativeExponent = pow(2.0, -2.0)       // 0.25

// 정수 거듭제곱 예시
let intResult = Int(pow(Double(5), Double(2))) // 25

profile
Reciprocity lies in knowing enough

2개의 댓글

comment-user-thumbnail
2024년 10월 31일

오… 메소드 체이닝을 저렇게 하니까 상당히 깰꼼하군요 내일 써봐야징

1개의 답글