Apa cara terbaik untuk menguji string kosong di Go?

261

Metode mana yang terbaik (lebih idomatik) untuk menguji string yang tidak kosong (dalam Go)?

if len(mystring) > 0 { }

Atau:

if mystring != "" { }

Atau sesuatu yang lain?

Richard
sumber

Jawaban:

390

Kedua gaya digunakan dalam perpustakaan standar Go.

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

dapat ditemukan dalam strconvpaket: http://golang.org/src/pkg/strconv/atoi.go

if s != "" { ... }

dapat ditemukan dalam encoding/jsonpaket: http://golang.org/src/pkg/encoding/json/encode.go

Keduanya idiomatis dan cukup jelas. Ini lebih merupakan masalah selera pribadi dan tentang kejelasan.

Russ Cox menulis di utas kacang kacangan :

Yang membuat kodenya jelas.
Jika saya akan melihat elemen x saya biasanya menulis
len (s)> x, bahkan untuk x == 0, tetapi jika saya peduli
"apakah ini string spesifik ini" Saya cenderung menulis s == "".

Masuk akal untuk mengasumsikan bahwa kompiler matang akan mengkompilasi
len (s) == 0 dan s == "" ke dalam kode yang sama dan efisien.
...

Buat kode menjadi jelas.

Seperti yang ditunjukkan dalam jawaban Timmmm , kompiler Go memang menghasilkan kode yang identik dalam kedua kasus.

Anisus
sumber
1
Saya tidak setuju dengan jawaban ini. Sederhananya if mystring != "" { }adalah cara terbaik, disukai dan idiomatik HARI INI. Alasan pustaka standar berisi sebaliknya adalah karena itu ditulis sebelum 2010 ketika len(mystring) == 0optimasi masuk akal.
honzajde
12
@honzajde Baru saja mencoba memvalidasi pernyataan Anda, tetapi menemukan komit di perpustakaan standar yang berusia kurang dari 1 tahun digunakan lenuntuk memeriksa string kosong / tidak kosong. Seperti ini yang dilakukan oleh Brad Fitzpatrick. Saya khawatir ini masih soal selera dan kejelasan;)
ANisus
6
@honzajde Tidak trolling. Ada 3 kata kunci dalam komit. Saya merujuk len(v) > 0pada h2_bundle.go (baris 2702). Itu tidak ditampilkan secara otomatis karena dihasilkan dari golang.org/x/net/http2, saya percaya.
ANisus
2
Jika ini adalah noi di diff maka itu bukan hal baru. Mengapa Anda tidak memposting tautan langsung? Bagaimanapun. pekerjaan detektif yang cukup untukku ... aku tidak melihatnya.
honzajde
6
@honzajde Jangan khawatir. Saya berasumsi orang lain akan tahu cara mengklik "Muat beda" untuk file h2_bundle.go.
ANisus
30

Ini tampaknya merupakan optimasi mikro prematur. Kompiler bebas untuk menghasilkan kode yang sama untuk kedua kasus atau setidaknya untuk keduanya

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

dan

if s != "" { ... }

karena semantiknya jelas sama.

zzzz
sumber
1
setuju, bagaimanapun, itu benar-benar tergantung pada implementasi string ... Jika string diimplementasikan seperti pascal maka len (s) dieksekusi dalam o (1) dan jika seperti C maka itu o (n). atau apa pun, karena len () harus dijalankan hingga selesai.
Richard
Sudahkah Anda melihat pembuatan kode untuk melihat apakah kompiler mengantisipasi ini atau apakah Anda hanya menyarankan kompiler dapat mengimplementasikan ini?
Michael Labbé
19

Memeriksa panjang adalah jawaban yang baik, tetapi Anda juga bisa menjelaskan string "kosong" yang juga hanya spasi kosong. Bukan "secara teknis" kosong, tetapi jika Anda ingin memeriksa:

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
sumber
TrimSpaceakan mengalokasikan dan menyalin string baru dari string asli, sehingga pendekatan ini akan memperkenalkan inefisiensi pada skala.
Dai
@ Hai melihat kode sumber, itu hanya akan benar jika, diberikan sadalah tipe string, s[0:i]mengembalikan salinan baru. String tidak dapat diubah di Go, jadi apakah itu perlu membuat salinan di sini?
Michael Paesold
@MichaelPaesold Right - strings.TrimSpace( s )tidak akan menyebabkan alokasi string baru dan karakter copy jika string tidak perlu pemangkasan, tetapi jika string memang perlu pemangkasan maka salinan tambahan (tanpa karakter spasi) akan dipanggil.
Dai
1
"secara teknis kosong" adalah pertanyaannya.
Richard
The gocriticlinter menyarankan menggunakan strings.TrimSpace(str) == ""bukannya cek panjang.
y3sh
12

Dengan asumsi bahwa ruang kosong dan semua spasi putih depan dan belakang harus dihilangkan:

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

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

Edwinner
sumber
2
Mengapa Anda memiliki asumsi ini? Pria itu dengan jelas bercerita tentang tali yang kosong. Cara yang sama yang dapat Anda katakan, dengan asumsi bahwa Anda hanya menginginkan karakter ascii dalam string dan kemudian menambahkan fungsi yang menghapus semua karakter non-ascii.
Salvador Dali
1
Karena len (""), len ("") dan len ("") bukan hal yang sama. Saya berasumsi dia ingin memastikan bahwa variabel yang dia inisialisasi ke salah satu dari yang sebelumnya memang masih "secara teknis" kosong.
Edwinner
Ini sebenarnya yang saya butuhkan dari posting ini. Saya membutuhkan input pengguna untuk memiliki setidaknya 1 karakter non-spasi dan baris satu ini jelas dan ringkas. Yang perlu saya lakukan adalah membuat if kondisi < 1+1
Shadoninja
7

Sampai sekarang, kompiler Go menghasilkan kode identik dalam kedua kasus, jadi ini adalah masalah selera. GCCGo memang menghasilkan kode yang berbeda, tetapi nyaris tidak ada yang menggunakannya jadi saya tidak akan khawatir tentang itu.

https://godbolt.org/z/fib1x1

Timmmm
sumber
1

Akan lebih bersih dan lebih rentan kesalahan untuk menggunakan fungsi seperti di bawah ini:

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

Hanya untuk menambahkan komentar

Terutama tentang bagaimana melakukan pengujian kinerja.

Saya melakukan pengujian dengan kode berikut:

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

Dan hasilnya adalah:

% 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

Varian yang efektif biasanya tidak mencapai waktu tercepat dan hanya ada perbedaan minimal (sekitar 0,01ns / op) antara varian top speed.

Dan jika saya melihat log lengkap, perbedaan antara percobaan lebih besar daripada perbedaan antara fungsi benchmark.

Juga sepertinya tidak ada perbedaan yang terukur antara BenchmarkStringCheckEq dan BenchmarkStringCheckNe atau BenchmarkStringCheckLen dan BenchmarkStringCheckLenGt bahkan jika varian terakhir harus termasuk 6 kali, bukannya 2 kali.

Anda dapat mencoba untuk mendapatkan kepercayaan diri tentang kinerja yang sama dengan menambahkan tes dengan tes yang dimodifikasi atau loop dalam. Ini lebih cepat:

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

Ini tidak lebih cepat:

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

Kedua varian biasanya lebih cepat atau lebih lambat daripada perbedaan antara tes utama.

Ini juga baik untuk menghasilkan string uji (ss) menggunakan generator string dengan distribusi yang relevan. Dan memiliki panjang variabel juga.

Jadi saya tidak memiliki keyakinan perbedaan kinerja antara metode utama untuk menguji string kosong.

Dan saya dapat menyatakan dengan percaya diri, lebih cepat untuk tidak menguji string kosong sama sekali daripada menguji string kosong. Dan juga lebih cepat untuk menguji string kosong daripada menguji 1 string char (varian awalan).

Markus Linnala
sumber
0

Sesuai pedoman resmi dan dari sudut pandang kinerja mereka tampak setara ( jawaban ANisus ), s! = "" Akan lebih baik karena keuntungan sintaksis. s! = "" akan gagal pada waktu kompilasi jika variabelnya bukan string, sementara len (s) == 0 akan lulus untuk beberapa tipe data lainnya.

Janis Viksne
sumber
Ada suatu masa ketika saya menghitung siklus CPU dan meninjau assembler yang dihasilkan oleh kompiler C dan sangat memahami struktur string C dan Pascal ... bahkan dengan semua optimasi di dunia len()hanya membutuhkan sedikit kerja ekstra. NAMUN, satu hal yang biasa kita lakukan di C adalah melemparkan sisi kiri ke a constatau meletakkan string statis di sisi kiri operator untuk mencegah s == "" dari menjadi s = "" yang dalam sintaks C dapat diterima. .. dan mungkin golang juga. (lihat diperpanjang jika)
Richard
-1

Ini akan lebih berkinerja daripada memangkas seluruh string, karena Anda hanya perlu memeriksa setidaknya satu karakter non-spasi yang ada

// 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
sumber
3
@Richard itu mungkin, tetapi ketika Googling untuk "golang memeriksa apakah string kosong" atau hal-hal serupa, ini adalah satu-satunya pertanyaan yang muncul, jadi bagi orang-orang ini untuk mereka, yang bukan merupakan hal yang belum pernah terjadi sebelumnya untuk dilakukan pada Stack Exchange
Brian Leishman
-1

Saya pikir cara terbaik adalah membandingkan dengan string kosong

BenchmarkStringCheck1 memeriksa dengan string kosong

BenchmarkStringCheck2 memeriksa dengan len nol

Saya memeriksa dengan memeriksa string kosong dan tidak kosong. Anda dapat melihat bahwa memeriksa dengan string kosong lebih cepat.

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

Kode

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
sumber
5
Saya pikir bukti ini tidak ada. Karena komputer Anda melakukan hal-hal lain ketika pengujian dan perbedaan kecil untuk mengatakan satu lebih cepat dari yang lain. Ini bisa mengisyaratkan bahwa kedua fungsi dikompilasi untuk panggilan yang sama.
SR