【问题标题】:func that can take multiple types可以采用多种类型的函数
【发布时间】:2018-06-08 12:52:33
【问题描述】:

我正在尝试创建一个辅助函数,它将获取带有字符串键的映射并返回映射键的一部分。

问题是我希望函数不关心地图值的类型。

例如:

stringStringMap := map[string]string{
    "one": "first",
    "two": "second"
}

mapKeys(stringStringMap) // ["one", "two"]

stringIntMap := map[string]int{
    "one": 1,
    "two": 2,
}

mapKeys(strinIntMap) // ["one", "two"]

似乎解决这个问题的唯一方法是创建两个类似的助手。像这样的:

func mapKeys(m map[string]string) []string {
    ...
}

func mapKeys2(m map[string]int) []string {
    ...
}

但这看起来很难看。我正在尝试创建的这个辅助函数是否可行?如果不是,在编写此代码时我应该遵循一个好的约定吗?

【问题讨论】:

  • 为什么投反对票?我应该如何更好地构建我的问题?
  • 我没有投反对票,但我想投反对票的原因很正常:缺乏研究工作。
  • 很公平,下次我会花更多时间在编程语言上,然后再问任何问题
  • 如果您将地图声明为map[string]interface{},那么您可以使用唯一函数迭代键(在所有情况下都是字符串)。

标签: go types syntax


【解决方案1】:

解决此问题的一种方法是在创建地图时使用“interface{}”,然后在方法中使用type switch 语句,即

func mapKeys(m map[string]interface{}) []string {
    for k,v := range m {
        switch a := v.(type) {
        case int:
          ... do int stuff
        case string:
          ... do string stuff
        }
    }
}

【讨论】:

    【解决方案2】:

    Go 没有用户指定的通用参数类型,因此您所描述的必须通过以下方式完成:

    1. 接受您希望支持的不同类型的单独函数,或
    2. 接受空接口 (interface{}) 并使用 reflection 的函数。

    这是第二种方法的示例实现:

    package main
    
    import (
        "fmt"
        "reflect"
    )
    
    func main() {
        stringStringMap := map[string]string{
            "one": "first",
            "two": "second",
        }
    
        fmt.Println(mapKeys(stringStringMap)) // ["one", "two"]
    
        stringIntMap := map[string]int{
            "one": 1,
            "two": 2,
        }
    
        fmt.Println(mapKeys(stringIntMap)) // ["one", "two"]
    }
    
    func mapKeys(m interface{}) []string {
        v := reflect.ValueOf(m)
        if v.Kind() != reflect.Map {
            panic("m is not a map")
        }
        if v.Type().Key().Kind() != reflect.String {
            panic("m does not have a string key")
        }
        keys := make([]string, 0, v.Len())
        for _, key := range v.MapKeys() {
            keys = append(keys, key.String())
        }
        return keys
    }
    

    【讨论】:

      【解决方案3】:

      还有第三个选项还没有提到,那就是使用类型开关。如果您知道将可管理数量的类型传递给函数,这可能是一个不错的选择。它会像这样工作:

      func Keys(m interface{}) ([]string, error) {
          switch t := m.(type) {
          case map[string]string:
              keys := make([]string, 0, len(t))
              for key := range t {
                  keys = append(keys, key)
              }
              return keys, nil
          case map[string]int:
              keys := make([]string, 0, len(t))
              for key := range t {
                  keys = append(keys, key)
              }
              return keys, nil
          default:
              return nil, fmt.Errorf("unknown map type: %T", m)
          }
      }
      

      这仍然会给你一堆看似重复的代码,但至少它们都在一个函数名后面,而且比反射更有效。

      【讨论】:

      • @coredump:这取决于视角。在屏幕上,代码看似重复,但语义和机器可读代码却没有,因为类型不同。第一个 for 循环实际上是 for var key string = ...,第二个是 for var key int = ...
      • @coredump:一些代码可能会被重复数据删除。 keys 可以在切换之前定义(但随后不知道要制作什么尺寸,影响效率),或者可以将return 移动到切换之后。不过,这两个选项都会损害可读性。但它们是选项,如果一个人想学究气 :) 但是 for 循环,尽管看起来是重复的,但实际上不是。
      • 看来,如果您想更改为string 构建keys 的方式并改用双向链表,那么您也将为int 更改它。这几乎就是代码重复的定义(wiki.c2.com/?DuplicatedCodeen.wikipedia.org/wiki/Duplicate_code)。即使克隆可能导致每个机器代码不同,但这并不会改变程序员操作的源代码是重复的事实;添加键的逻辑是相同的。另外,我不确定范围是否不同:毕竟,您只迭代键。
      • @coredump:欢迎您提供自己的更好的答案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多