Jak działa String.Index w języku Swift

109

Aktualizowałem część mojego starego kodu i odpowiedzi w Swift 3, ale kiedy dotarłem do Swift Strings i indeksowania, zrozumienie rzeczy było trudne.

W szczególności próbowałem następujących rzeczy:

let str = "Hello, playground"
let prefixRange = str.startIndex..<str.startIndex.advancedBy(5) // error

gdzie druga linia dawała mi następujący błąd

Opcja „advancedBy” jest niedostępna: aby przyspieszyć indeks o n kroków, wywołaj „index (_: offsetBy :)” w instancji CharacterView, która wygenerowała indeks.

Widzę, że Stringma następujące metody.

str.index(after: String.Index)
str.index(before: String.Index)
str.index(String.Index, offsetBy: String.IndexDistance)
str.index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

Na początku naprawdę mnie to zdezorientowało, więc zacząłem się nimi bawić, dopóki ich nie zrozumiałem. Poniżej dodaję odpowiedź, aby pokazać, jak są używane.

Suragch
źródło

Odpowiedzi:

281

wprowadź opis obrazu tutaj

Wszystkie poniższe przykłady używają

var str = "Hello, playground"

startIndex i endIndex

  • startIndex jest indeksem pierwszego znaku
  • endIndexjest indeksem po ostatnim znaku.

Przykład

// character
str[str.startIndex] // H
str[str.endIndex]   // error: after last character

// range
let range = str.startIndex..<str.endIndex
str[range]  // "Hello, playground"

Dzięki jednostronnym seriom Swift 4 , asortyment można uprościć do jednej z następujących form.

let range = str.startIndex...
let range = ..<str.endIndex

W celu zachowania przejrzystości użyję pełnej formy w poniższych przykładach, ale ze względu na czytelność prawdopodobnie zechcesz użyć jednostronnych zakresów w swoim kodzie.

after

Jak w: index(after: String.Index)

  • after odnosi się do indeksu znaku bezpośrednio po danym indeksie.

Przykłady

// character
let index = str.index(after: str.startIndex)
str[index]  // "e"

// range
let range = str.index(after: str.startIndex)..<str.endIndex
str[range]  // "ello, playground"

before

Jak w: index(before: String.Index)

  • before odnosi się do indeksu znaku bezpośrednio przed danym indeksem.

Przykłady

// character
let index = str.index(before: str.endIndex)
str[index]  // d

// range
let range = str.startIndex..<str.index(before: str.endIndex)
str[range]  // Hello, playgroun

offsetBy

Jak w: index(String.Index, offsetBy: String.IndexDistance)

  • offsetByWartość może być dodatnia lub ujemna, a rozpoczyna się od danego indeksu. Chociaż jest tego typu String.IndexDistance, możesz nadać mu plik Int.

Przykłady

// character
let index = str.index(str.startIndex, offsetBy: 7)
str[index]  // p

// range
let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end
str[range]  // play

limitedBy

Jak w: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • limitedByJest przydatny do upewniając się, że nie powoduje przesunięcia indeksu, aby przejść poza granicami. Jest to indeks ograniczający. Ponieważ przesunięcie może przekroczyć limit, ta metoda zwraca parametr Optional. Zwraca, niljeśli indeks jest poza zakresem.

Przykład

// character
if let index = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
    str[index]  // p
}

Gdyby offset był 77zamiast 7, to ifinstrukcja zostałaby pominięta.

Dlaczego potrzebny jest String.Index?

Byłoby znacznie łatwiej używać Intindeksu dla pszczół. Powodem, dla którego musisz utworzyć nowy String.Indexdla każdego łańcucha, jest to, że postacie w języku Swift nie mają takiej samej długości pod maską. Pojedynczy znak Swift może składać się z jednego, dwóch lub nawet więcej punktów kodowych Unicode. Dlatego każdy unikalny ciąg musi obliczyć indeksy swoich znaków.

Jest to możliwe, aby ukryć tę złożoność za rozszerzeniem indeksu Int, ale niechętnie to robię. Warto przypomnieć sobie, co się naprawdę dzieje.

Suragch
źródło
18
Dlaczego miałoby startIndexbyć cokolwiek innego niż 0?
Robo Robok,
15
@RoboRobok: Ponieważ Swift działa ze znakami Unicode, które są zbudowane z „klastrów grafemowych”, Swift nie używa liczb całkowitych do reprezentowania lokalizacji indeksów. Powiedzmy, że twoja pierwsza postać to é. W rzeczywistości składa się z eplusa \u{301}reprezentacji Unicode. Jeśli użyjesz indeksu zerowego, otrzymasz eznak lub znak akcentu („poważny”), a nie cały klaster, który tworzy é. Użycie tego startIndexgwarantuje, że otrzymasz cały klaster grafemów dla dowolnej postaci.
leanne
2
W Swift 4.0 każdy znak Unicode jest liczony przez 1. Np .: "👩‍💻" .count // Teraz: 1, Przed: 2
selva
3
W jaki sposób można skonstruować a String.Indexz liczby całkowitej, inaczej niż zbudowanie fikcyjnego ciągu i użycie na nim .indexmetody? Nie wiem, czy czegoś mi brakuje, ale doktorzy nic nie mówią.
sudo
3
@sudo, musisz być trochę ostrożny podczas konstruowania a String.Indexz liczbą całkowitą, ponieważ każdy Swift Characterniekoniecznie oznacza to samo, co masz na myśli z liczbą całkowitą. To powiedziawszy, możesz przekazać liczbę całkowitą do offsetByparametru, aby utworzyć plik String.Index. Jeśli jednak nie masz a String, nie możesz skonstruować a String.Index(ponieważ Swift może obliczyć indeks tylko wtedy, gdy wie, jakie są poprzednie znaki w ciągu). Jeśli zmienisz ciąg, musisz ponownie obliczyć indeks. Nie możesz użyć tego samego String.Indexna dwóch różnych strunach.
Suragch
0
func change(string: inout String) {

    var character: Character = .normal

    enum Character {
        case space
        case newLine
        case normal
    }

    for i in stride(from: string.count - 1, through: 0, by: -1) {
        // first get index
        let index: String.Index?
        if i != 0 {
            index = string.index(after: string.index(string.startIndex, offsetBy: i - 1))
        } else {
            index = string.startIndex
        }

        if string[index!] == "\n" {

            if character != .normal {

                if character == .newLine {
                    string.remove(at: index!)
                } else if character == .space {
                    let number = string.index(after: string.index(string.startIndex, offsetBy: i))
                    if string[number] == " " {
                        string.remove(at: number)
                    }
                    character = .newLine
                }

            } else {
                character = .newLine
            }

        } else if string[index!] == " " {

            if character != .normal {

                string.remove(at: index!)

            } else {
                character = .space
            }

        } else {

            character = .normal

        }

    }

    // startIndex
    guard string.count > 0 else { return }
    if string[string.startIndex] == "\n" || string[string.startIndex] == " " {
        string.remove(at: string.startIndex)
    }

    // endIndex - here is a little more complicated!
    guard string.count > 0 else { return }
    let index = string.index(before: string.endIndex)
    if string[index] == "\n" || string[index] == " " {
        string.remove(at: index)
    }

}
Pavel Zavarin
źródło
-2

Utwórz UITextView wewnątrz tableViewController. Użyłem funkcji: textViewDidChange, a następnie sprawdziłem, czy dane wejściowe są zwracane. następnie jeśli wykryje wejście klawisza powrotu, usuń wejście klawisza powrotu i zamknij klawiaturę.

func textViewDidChange(_ textView: UITextView) {
    tableView.beginUpdates()
    if textView.text.contains("\n"){
        textView.text.remove(at: textView.text.index(before: textView.text.endIndex))
        textView.resignFirstResponder()
    }
    tableView.endUpdates()
}
Karl Mogensen
źródło