Quelle est la meilleure façon de tester une chaîne vide dans Go?

261

Quelle méthode est la meilleure (plus idomatique) pour tester des chaînes non vides (dans Go)?

if len(mystring) > 0 { }

Ou:

if mystring != "" { }

Ou autre chose?

Richard
la source

Réponses:

390

Les deux styles sont utilisés dans les bibliothèques standard de Go.

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

peut être trouvé dans le strconvpackage: http://golang.org/src/pkg/strconv/atoi.go

if s != "" { ... }

peut être trouvé dans le encoding/jsonpackage: http://golang.org/src/pkg/encoding/json/encode.go

Les deux sont idiomatiques et assez clairs. C'est plus une question de goût personnel et de clarté.

Russ Cox écrit dans un fil de noix de golang :

Celui qui rend le code clair.
Si je suis sur le point de regarder l'élément x, j'écris généralement
len (s)> x, même pour x == 0, mais si je me soucie de
"est-ce cette chaîne spécifique", j'ai tendance à écrire s == "".

Il est raisonnable de supposer qu'un compilateur mature compilera
len (s) == 0 et s == "" dans le même code efficace.
...

Rendez le code clair.

Comme indiqué dans la réponse de Timmmm , le compilateur Go génère du code identique dans les deux cas.

ANisus
la source
1
Je ne suis pas d'accord avec cette réponse. C'est simplement if mystring != "" { }la meilleure façon, préférée et idiomatique AUJOURD'HUI. La raison pour laquelle la bibliothèque standard contient autre chose est qu'elle a été écrite avant 2010 lorsque l' len(mystring) == 0optimisation était logique.
honzajde
12
@honzajde J'ai juste essayé de valider votre déclaration, mais j'ai trouvé des validations dans la bibliothèque standard de moins d'un an en utilisant lenpour vérifier les chaînes vides / non vides. Comme cet engagement de Brad Fitzpatrick. J'ai bien peur que ce soit encore une question de goût et de clarté;)
ANisus
6
@honzajde Pas à la traîne. Il y a 3 len mots clés dans le commit. Je faisais allusion à len(v) > 0dans h2_bundle.go (ligne 2702). Il ne s'affiche pas automatiquement car il est généré à partir de golang.org/x/net/http2, je crois.
ANisus
2
Si c'est pas dans le diff alors ce n'est pas nouveau. Pourquoi ne publiez-vous pas de lien direct? Quoi qu'il en soit. assez de travail de détective pour moi ... Je ne le vois pas.
honzajde
6
@honzajde Pas de souci. Je suppose que d'autres sauront comment cliquer sur "Charger diff" pour le fichier h2_bundle.go.
ANisus
30

Cela semble être une microoptimisation prématurée. Le compilateur est libre de produire le même code pour les deux cas ou au moins pour ces deux

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

et

if s != "" { ... }

parce que la sémantique est clairement égale.

zzzz
la source
1
d'accord, cependant, cela dépend vraiment de l'implémentation de la chaîne ... Si les chaînes sont implémentées comme pascal alors len (s) est exécuté en o (1) et si comme C alors c'est o (n). ou quoi que ce soit, puisque len () doit s'exécuter complètement.
Richard
Avez-vous regardé la génération de code pour voir si le compilateur anticipe cela ou suggérez-vous seulement qu'un compilateur pourrait implémenter cela?
Michael Labbé
19

La vérification de la longueur est une bonne réponse, mais vous pouvez également prendre en compte une chaîne "vide" qui n'est également qu'un espace blanc. Pas "techniquement" vide, mais si vous voulez vérifier:

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!")
  }
}
Wilhelm Murdoch
la source
TrimSpaceva allouer et copier une nouvelle chaîne à partir de la chaîne d'origine, donc cette approche introduira des inefficacités à grande échelle.
Dai
@Dai en regardant le code source, cela ne serait vrai que si, étant donné sest de type chaîne, s[0:i]retourne une nouvelle copie. Les chaînes sont immuables dans Go, faut-il donc en créer une copie ici?
Michael Paesold
@MichaelPaesold Right - strings.TrimSpace( s )n'entraînera pas une nouvelle allocation de chaîne et une nouvelle copie de caractères si la chaîne n'a pas besoin d'être coupée, mais si la chaîne a besoin d'être coupée, la copie supplémentaire (sans les espaces) sera invoquée.
Dai
1
"techniquement vide" est la question.
Richard
Le gocriticlinter suggère d'utiliser à la strings.TrimSpace(str) == ""place du contrôle de longueur.
y3sh
12

En supposant que les espaces vides et tous les espaces blancs de début et de fin doivent être supprimés:

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

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

Edwinner
la source
2
Pourquoi avez-vous cette hypothèse? Le gars parle clairement de la chaîne vide. De la même manière que vous pouvez le dire, en supposant que vous ne voulez que des caractères ascii dans une chaîne, puis ajoutez une fonction qui supprime tous les caractères non ascii.
Salvador Dali
1
Parce que len (""), len ("") et len ​​("") ne sont pas la même chose. Je supposais qu'il voulait s'assurer qu'une variable qu'il avait initialisée à l'une de celles précédentes était en effet toujours "techniquement" vide.
Edwinner
C'est exactement ce dont j'avais besoin dans ce post. J'ai besoin de l'entrée utilisateur pour avoir au moins 1 caractère non blanc et ce one-liner est clair et concis. Tout ce que je dois faire est de faire la condition if < 1+1
Shadoninja
7

À partir de maintenant, le compilateur Go génère du code identique dans les deux cas, c'est donc une question de goût. GCCGo génère un code différent, mais presque personne ne l'utilise, donc je ne m'inquiéterais pas de cela.

https://godbolt.org/z/fib1x1

Timmmm
la source
1

Il serait plus propre et moins sujet aux erreurs d'utiliser une fonction comme celle ci-dessous:

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

Juste pour ajouter plus de commentaires

Principalement sur la façon de faire des tests de performances.

J'ai fait des tests avec le code suivant:

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)
    }
}

Et les résultats ont été:

% 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

En effet, les variantes n'atteignent généralement pas le temps le plus rapide et il n'y a qu'une différence minimale (environ 0,01 ns / op) entre la vitesse de pointe des variantes.

Et si je regarde le journal complet, la différence entre les essais est supérieure à la différence entre les fonctions de référence.

De plus, il ne semble pas y avoir de différence mesurable entre BenchmarkStringCheckEq et BenchmarkStringCheckNe ou BenchmarkStringCheckLen et BenchmarkStringCheckLenGt même si ces dernières variantes doivent augmenter c 6 fois au lieu de 2 fois.

Vous pouvez essayer d'obtenir une certaine confiance sur des performances égales en ajoutant des tests avec un test modifié ou une boucle interne. C'est plus rapide:

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)
    }
}

Ce n'est pas plus rapide:

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)
    }
}

Les deux variantes sont généralement plus rapides ou plus lentes que la différence entre les tests principaux.

Il serait également bon de générer des chaînes de test (ss) en utilisant un générateur de chaînes avec une distribution appropriée. Et ont également des longueurs variables.

Je n'ai donc aucune confiance en la différence de performances entre les principales méthodes pour tester une chaîne vide en cours.

Et je peux affirmer avec une certaine confiance, il est plus rapide de ne pas tester la chaîne vide du tout que de tester la chaîne vide. Et il est également plus rapide de tester une chaîne vide que de tester 1 chaîne de caractères (variante de préfixe).

Markus Linnala
la source
0

Selon les directives officielles et du point de vue des performances, ils semblent équivalents ( réponse ANisus ), le s! = "" Serait préférable en raison d'un avantage syntaxique. s! = "" échouera au moment de la compilation si la variable n'est pas une chaîne, tandis que len (s) == 0 passera pour plusieurs autres types de données.

Janis Viksne
la source
Il fut un temps où je comptais les cycles de processeur et passais en revue l'assembleur que le compilateur C produisait et comprenait profondément la structure des chaînes C et Pascal ... même avec toutes les optimisations dans le monde, il len()suffit de ce petit peu de travail supplémentaire. TOUTEFOIS, une chose que nous avions l'habitude de faire en C était de convertir le côté gauche en a constou de mettre la chaîne statique du côté gauche de l'opérateur pour empêcher s == "" de devenir s = "" ce qui dans la syntaxe C est acceptable. .. et probablement golang aussi. (voir l'extension si)
Richard
-1

Ce serait plus performant que de rogner la chaîne entière, car il vous suffit de vérifier au moins un seul caractère non espace existant

// 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
}
Brian Leishman
la source
3
@Richard cela peut être, mais lorsque Google recherche "golang vérifie si la chaîne est vide" ou des choses similaires, c'est la seule question qui se pose, donc pour ces personnes, c'est pour eux, ce qui n'est pas une chose sans précédent à faire sur Échange de pile
Brian Leishman
-1

Je pense que la meilleure façon est de comparer avec une chaîne vide

BenchmarkStringCheck1 vérifie avec une chaîne vide

BenchmarkStringCheck2 vérifie avec len zéro

Je vérifie avec la vérification de chaîne vide et non vide. Vous pouvez voir que la vérification avec une chaîne vide est plus rapide.

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

Code

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 {

        }
    }
}
Ketan Parmar
la source
5
Je pense que cette preuve rien. Puisque votre ordinateur fait d'autres choses lors des tests et que la différence est trop petite pour dire que l'un est plus rapide que l'autre. Cela pourrait suggérer que les deux fonctions ont été compilées pour le même appel.
SR