Jaki jest najlepszy sposób przetestowania pustego ciągu w Go?


260

Która metoda jest najlepsza (bardziej idomatyczna) do testowania niepustych ciągów (w Go)?

if len(mystring) > 0 { }

Lub:

if mystring != "" { }

Albo coś innego?

Odpowiedzi:


388

Oba style są używane w standardowych bibliotekach Go.

if len(s) > 0 { ... }

można znaleźć w strconvpakiecie: http://golang.org/src/pkg/strconv/atoi.go

if s != "" { ... }

można znaleźć w encoding/jsonpakiecie: http://golang.org/src/pkg/encoding/json/encode.go

Oba są idiomatyczne i wystarczająco jasne. Jest to bardziej kwestia osobistego gustu i przejrzystości.

Russ Cox pisze w wątku golang-nuts :

Ten, który wyjaśnia kod.
Jeśli mam spojrzeć na element x, zwykle piszę
len (s)> x, nawet dla x == 0, ale jeśli mnie to obchodzi
„czy to ten konkretny ciąg„ mam tendencję do pisania s == ””.

Można założyć, że dojrzały kompilator skompiluje
len (s) == 0 is s == "" do tego samego, wydajnego kodu.
...

Wyczyść kod.

Jak wskazano w odpowiedzi Timmmm , kompilator Go generuje identyczny kod w obu przypadkach.


1
Nie zgadzam się z tą odpowiedzią. Po prostu if mystring != "" { }jest to najlepszy, preferowany i idiomatyczny sposób DZISIAJ. Powodem, dla którego standardowa biblioteka zawiera inaczej, jest to, że została napisana przed 2010 r., Kiedy len(mystring) == 0optymalizacja miała sens.
honzajde

12
@honzajde Próbowałem tylko sprawdzić poprawność twojego oświadczenia, ale znalazłem zatwierdzenia w standardowej bibliotece mniejszej niż 1 rok, używając lendo sprawdzania pustych / niepustych ciągów. Tak jak to zrobił Brad Fitzpatrick. Obawiam się, że wciąż jest to kwestia gustu i jasności;)
ANisus

6
@honzajde Not trolling. W zatwierdzeniu znajdują się 3 słowa kluczowe len. Mam na myśli len(v) > 0w h2_bundle.go (linia 2702). Uważam, że nie jest automatycznie wyświetlany, ponieważ jest generowany z golang.org/x/net/http2.
ANisus

2
Jeśli w pliku różnic jest noi, to nie jest nowy. Dlaczego nie publikujesz bezpośredniego linku? Tak czy inaczej. wystarczająco dużo pracy detektywistycznej dla mnie ... nie widzę tego.
honzajde

6
@honzajde Nie martw się. Zakładam, że inni będą wiedzieć, jak kliknąć „Load diff” dla pliku h2_bundle.go.
ANisus

30

To wydaje się być przedwczesną mikrooptymalizacją. Kompilator może generować ten sam kod dla obu przypadków lub przynajmniej dla tych dwóch

if len(s) != 0 { ... }

i

if s != "" { ... }

ponieważ semantyka jest wyraźnie równa.


1
zgadza się jednak, że tak naprawdę zależy to od implementacji łańcucha ... Jeśli łańcuchy są implementowane jak pascal, wówczas len (s) jest wykonywane w o (1), a jeśli tak jak w C, to jest to o (n). lub cokolwiek innego, ponieważ len () musi zostać wykonane do końca.
Richard

Czy spojrzałeś na generowanie kodu, aby sprawdzić, czy kompilator przewiduje to, czy sugerujesz tylko, że kompilator mógłby to zaimplementować?
Michael Labbé

19

Sprawdzanie długości jest dobrą odpowiedzią, ale można również uwzględnić „pusty” ciąg znaków, który jest również tylko białymi spacjami. Nie „technicznie” pusty, ale jeśli chcesz sprawdzić:

package main

import (
  "fmt"
  "strings"
)

func main() {
  stringOne := "merpflakes"
  stringTwo := "   "
  stringThree := ""

  if len(strings.TrimSpace(stringOne)) == 0 {
    fmt.Println("String is empty!")
  }

  if len(strings.TrimSpace(stringTwo)) == 0 {
    fmt.Println("String two is empty!")
  }

  if len(stringTwo) == 0 {
    fmt.Println("String two is still empty!")
  }

  if len(strings.TrimSpace(stringThree)) == 0 {
    fmt.Println("String three is empty!")
  }
}

TrimSpaceprzydzieli i skopiuje nowy ciąg z oryginalnego ciągu, więc to podejście wprowadzi nieefektywności na dużą skalę.
Dai,

@Dai patrząc na kod źródłowy, byłoby to prawdą tylko wtedy, gdy podany sciąg typu s[0:i]zwraca nową kopię. Ciągi są niezmienne w Go, więc czy trzeba tutaj utworzyć kopię?
Michael Paesold

@MichaelPaesold Right - strings.TrimSpace( s )nie spowoduje przydzielenia nowego ciągu i kopiowania znaków, jeśli ciąg nie wymaga przycinania, ale jeśli ciąg wymaga przycinania, zostanie wywołana dodatkowa kopia (bez znaków spacji).
Dai

1
pytanie brzmi „technicznie pusty”.
Richard

gocriticLinter sugeruje użycie strings.TrimSpace(str) == ""zamiast czeku długości.
y3sh 16.10.19

12

Zakładając, że należy usunąć puste spacje oraz wszystkie wiodące i końcowe białe spacje:

import "strings"
if len(strings.TrimSpace(s)) == 0 { ... }

Ponieważ :
len("") // is 0
len(" ") // one empty space is 1
len(" ") // two empty spaces is 2


2
Dlaczego masz takie założenie? Facet wyraźnie mówi o pustym sznurku. W ten sam sposób możesz powiedzieć, zakładając, że chcesz tylko znaków ascii w ciągu, a następnie dodaj funkcję, która usuwa wszystkie znaki inne niż ascii.
Salvador Dali,

1
Ponieważ len („”), len („”) i len („”) to nie to samo. Zakładałem, że chciał się upewnić, że zmienna, którą zainicjował na jedną z tych wcześniejszych, jest rzeczywiście „technicznie” pusta.
Edwinner,

Właśnie tego potrzebowałem od tego postu. Potrzebuję wkładu użytkownika, aby mieć co najmniej 1 znak spacji, a ten jednowarstwowy jest przejrzysty i zwięzły. Wszystko, co muszę zrobić, to zrobić warunek if < 1+1
Shadoninja

7

Na razie kompilator Go generuje identyczny kod w obu przypadkach, więc jest to kwestia gustu. GCCGo generuje inny kod, ale prawie nikt go nie używa, więc nie martwię się o to.

https://godbolt.org/z/fib1x1


1

Użycie funkcji takiej jak ta poniżej byłoby czystsze i mniej podatne na błędy:

func empty(s string) bool {
    return len(strings.TrimSpace(s)) == 0
}

0

Aby dodać więcej do komentarza

Głównie o tym, jak przeprowadzić testy wydajności.

Testowałem przy użyciu następującego kodu:

import (
    "testing"
)

var ss = []string{"Hello", "", "bar", " ", "baz", "ewrqlosakdjhf12934c r39yfashk fjkashkfashds fsdakjh-", "", "123"}

func BenchmarkStringCheckEq(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss {
                    if s == "" {
                            c++
                    }
            }
    } 
    t := 2 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}
func BenchmarkStringCheckLen(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss { 
                    if len(s) == 0 {
                            c++
                    }
            }
    } 
    t := 2 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}
func BenchmarkStringCheckLenGt(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss {
                    if len(s) > 0 {
                            c++
                    }
            }
    } 
    t := 6 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}
func BenchmarkStringCheckNe(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss {
                    if s != "" {
                            c++
                    }
            }
    } 
    t := 6 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}

I wyniki były:

% for a in $(seq 50);do go test -run=^$ -bench=. --benchtime=1s ./...|grep Bench;done | tee -a log
% sort -k 3n log | head -10

BenchmarkStringCheckEq-4        150149937            8.06 ns/op
BenchmarkStringCheckLenGt-4     147926752            8.06 ns/op
BenchmarkStringCheckLenGt-4     148045771            8.06 ns/op
BenchmarkStringCheckNe-4        145506912            8.06 ns/op
BenchmarkStringCheckLen-4       145942450            8.07 ns/op
BenchmarkStringCheckEq-4        146990384            8.08 ns/op
BenchmarkStringCheckLenGt-4     149351529            8.08 ns/op
BenchmarkStringCheckNe-4        148212032            8.08 ns/op
BenchmarkStringCheckEq-4        145122193            8.09 ns/op
BenchmarkStringCheckEq-4        146277885            8.09 ns/op

Skutecznie warianty zwykle nie osiągają najszybszego czasu i istnieje tylko minimalna różnica (około 0,01ns / operacja) między wariantem prędkości maksymalnej.

A jeśli zajrzę do pełnego dziennika, różnica między próbami jest większa niż różnica między funkcjami testu porównawczego.

Nie wydaje się również, aby istniała jakakolwiek mierzalna różnica między BenchmarkStringCheckEq i BenchmarkStringCheckNe lub BenchmarkStringCheckLen i BenchmarkStringCheckLenGt, nawet jeśli te ostatnie warianty powinny zawierać c 6 razy zamiast 2 razy.

Możesz spróbować uzyskać pewność co do równej wydajności, dodając testy ze zmodyfikowanym testem lub pętlą wewnętrzną. Jest to szybsze:

func BenchmarkStringCheckNone4(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, _ = range ss {
                    c++
            }
    }
    t := len(ss) * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}

To nie jest szybsze:

func BenchmarkStringCheckEq3(b *testing.B) {
    ss2 := make([]string, len(ss))
    prefix := "a"
    for i, _ := range ss {
            ss2[i] = prefix + ss[i]
    }
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss2 {
                    if s == prefix {
                            c++
                    }
            }
    }
    t := 2 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}

Oba warianty są zwykle szybsze lub wolniejsze niż różnica między głównymi testami.

Dobrze byłoby również wygenerować łańcuchy testowe (ss) przy użyciu generatora łańcuchów o odpowiednim rozkładzie. I mają też różne długości.

Więc nie mam żadnej pewności co do różnicy wydajności między głównymi metodami testowania pustego łańcucha w ruchu.

Mogę stwierdzić z pewną pewnością, że szybciej nie testować pustego łańcucha niż testować pusty łańcuch. A także szybsze jest testowanie pustego łańcucha niż testowanie 1 łańcucha znaków (wariant prefiksu).


0

Zgodnie z oficjalnymi wytycznymi iz punktu widzenia wydajności wydają się one równoważne ( odpowiedź ANisus ), s! = "" Byłoby lepsze ze względu na przewagę syntaktyczną. s! = "" zakończy się niepowodzeniem w czasie kompilacji, jeśli zmienna nie jest łańcuchem, a len (s) == 0 przejdzie dla kilku innych typów danych.


Był czas, kiedy liczyłem cykle procesora i sprawdzałem asembler, który kompilator C stworzył i głęboko zrozumiał strukturę ciągów C i Pascala ... nawet przy wszystkich optymalizacjach na świecie len()wymaga tylko trochę więcej pracy. JEDNAK, jedną rzeczą, którą robiliśmy w C, było przeniesienie lewej strony na a constlub umieszczenie ciągu statycznego po lewej stronie operatora, aby zapobiec s == "" zamienianiu się w s = "", co w składni C jest dopuszczalne. .. i prawdopodobnie również Golanga. (patrz przedłużony, jeśli)
Richard

-1

Byłoby to bardziej wydajne niż przycinanie całego łańcucha, ponieważ wystarczy sprawdzić przynajmniej jeden istniejący znak spacji

// Strempty checks whether string contains only whitespace or not
func Strempty(s string) bool {
    if len(s) == 0 {
        return true
    }

    r := []rune(s)
    l := len(r)

    for l > 0 {
        l--
        if !unicode.IsSpace(r[l]) {
            return false
        }
    }

    return true
}

3
@ Może to być Richard, ale kiedy Googling dla „golanga sprawdza, czy łańcuch jest pusty” lub czegoś podobnego, pojawia się jedyne pytanie, więc dla tych ludzi jest to dla nich, co nie jest niczym niespotykanym Wymiana
stosów

-1

Myślę, że najlepszym sposobem jest porównanie z pustym ciągiem

BenchmarkStringCheck1 sprawdza pusty ciąg znaków

BenchmarkStringCheck2 sprawdza z długością zerową

Sprawdzam za pomocą sprawdzania pustych i niepustych ciągów. Widać, że sprawdzanie pustym ciągiem jest szybsze.

BenchmarkStringCheck1-4     2000000000           0.29 ns/op        0 B/op          0 allocs/op
BenchmarkStringCheck1-4     2000000000           0.30 ns/op        0 B/op          0 allocs/op


BenchmarkStringCheck2-4     2000000000           0.30 ns/op        0 B/op          0 allocs/op
BenchmarkStringCheck2-4     2000000000           0.31 ns/op        0 B/op          0 allocs/op

Kod

func BenchmarkStringCheck1(b *testing.B) {
    s := "Hello"
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        if s == "" {

        }
    }
}

func BenchmarkStringCheck2(b *testing.B) {
    s := "Hello"
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        if len(s) == 0 {

        }
    }
}

5
Myślę, że ten dowód nic. Ponieważ twój komputer robi inne rzeczy podczas testowania, a różnica jest zbyt mała, aby powiedzieć, że jedna jest szybsza od drugiej. Może to sugerować, że obie funkcje zostały skompilowane do tego samego wywołania.
SR
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.