Scala Case Class Tupled

9

如何在这个case class上调用tupled方法?

case class(a: Int, b: String)(c: String, d: Int)

我之所以将我的案例类设计成这样,是因为我希望只使用前两个参数进行equals和hashCode比较!

那么我该如何正确地在这样的案例类上调用tupled方法?


1
为什么你想要将这个定义为Case类?Case类有一些便利特性,但代价是一定的灵活性损失。你可以自己定义一个类并重写equals/hashCode方法。 - The Archetypal Paul
是的,我明白,但是在这个 case class 上我要进行一些复杂的模式匹配,所以我必须将它保留为 case class! - joesan
1
也许您想将问题重命名为“Scala Case Class多个参数列表”,而不是Tupled。 - yǝsʞǝla
然后编写一个unapply方法。关于case类并没有什么特别神奇的地方,你可以自己实现相应的功能。 - The Archetypal Paul
1个回答

13

简而言之,以这种方式使用case类似乎不是一个好主意。以下是解释。

让我们一起检查类声明和生成的 apply 和 unapply:

scala> case class A(a: Int, b: String)(c: String, d: Int)
defined class A

scala> A.apply _
res0: (Int, String) => (String, Int) => A = <function2>

scala> A.unapply _
res1: A => Option[(Int, String)] = <function1>
你可以看到,尽管apply总共需要4个参数(柯里化),unapply却会忽略第二个列表。
让我们看看是否可以访问第二个列表中的成员:
scala> val a = A(1, "two")("three", 4)
a: A = A(1,two)

scala> a.a
res2: Int = 1

scala> a.c
<console>:11: error: value c is not a member of A
              a.c
                ^

......不对,这不是常规方式。让我们再检查几个属性:

scala> a.productArity
res4: Int = 2

scala> a.productIterator.toList
res5: List[Any] = List(1, two)

好的,看起来第二个参数列表被完全忽略了。让我们深入探讨一下:

scala> :javap A
...
  public int a();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #16                 // Field a:I
         4: ireturn
...
  public java.lang.String b();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #21                 // Field b:Ljava/lang/String;
         4: areturn
...
  public boolean equals(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Z
    flags: ACC_PUBLIC
     ... //mentions only a and b:....
        32: invokevirtual #32                 // Method a:()I
        35: aload         4
        37: invokevirtual #32                 // Method a:()I
        40: if_icmpne     88
        43: aload_0
        44: invokevirtual #35                 // Method b:()Ljava/lang/String;
        47: aload         4
        49: invokevirtual #35                 // Method b:()Ljava/lang/String;
...
  public A(int, java.lang.String, java.lang.String, int);                                                                 
    descriptor: (ILjava/lang/String;Ljava/lang/String;I)V                                                                 
    flags: ACC_PUBLIC                                                                                                     
    Code:                                                                                                                 
      stack=2, locals=5, args_size=5                                                                                      
         0: aload_0                                                                                                       
         1: iload_1                                                                                                       
         2: putfield      #16                 // Field a:I                                                                
         5: aload_0                                                                                                       
         6: aload_2                                                                                                       
         7: putfield      #21                 // Field b:Ljava/lang/String;                                               
        10: aload_0                                                                                                       
        11: invokespecial #100                // Method java/lang/Object."<init>":()V                                     
        14: aload_0                                                                                                       
        15: invokestatic  #106                // Method scala/Product$class.$init$:(Lscala/Product;)V                     
        18: return                                                                                                        
      LocalVariableTable:                                                                                                 
        Start  Length  Slot  Name   Signature                                                                             
            0      19     0  this   LA;                                                                                   
            0      19     1     a   I                                                                                     
            0      19     2     b   Ljava/lang/String;                                                                    
            0      19     3     c   Ljava/lang/String;                                                                    
            0      19     4     d   I                                                                                  

因此,在构造函数或equals方法中没有使用 c 和 d 没有任何用处。

您仍然可以通过在第二个参数列表参数前加上 val 来使它们有用:


scala> case class B(a: Int, b: String)(val c: String, val d: Int)         
defined class B                                                           

scala> val b = B(1, "two")("three", 4)                                    
b: B = B(1,two)                                                           

scala> b.c                                                                
res6: String = three                                  

scala> b.d                                                                
res8: Int = 4    

让我们看看在这种情况下相等性和哈希码如何工作:

scala> val b2 = B(1, "two")("no the same", 555)
b2: B = B(1,two)

scala> b == b2
res10: Boolean = true

scala> b.hashCode
res13: Int = -1563217100

scala> b2.hashCode
res14: Int = -1563217100

看起来它按照你想的方式工作,但我个人不太喜欢 ;)

为了完整起见,默认模式匹配仍然与类A之前一样:

scala> B.unapply _
res15: B => Option[(Int, String)] = <function1>

Scala语言规范在这里解释了它是如何工作的。

在case class的第一个参数部分中的正式参数被称为元素,它们被特殊处理。首先,这样一个参数的值可以作为构造函数模式的一个字段来提取。其次,val前缀被隐式地添加到这样的参数上,除非参数已经带有valvar修饰符。因此,生成了一个参数的访问器定义。

并且

每个case类都会隐式地覆盖scala.AnyRef类的一些方法定义,除非在case类本身或者case类不同于AnyRef的某个基类中给出了相同方法的定义。特别地:

  • 方法equals:(Any)Boolean是结构相等性,当两个实例都属于所讨论的case类时,并且它们有相等的(关于equals)构造函数参数(限定在类的元素上,即第一个参数部分)时,它们相等。
  • 方法hashCode: Int计算哈希码。如果数据结构成员的hashCode方法将相等的(关于equals)值映射到相等的哈希码,则case类hashCode方法也是如此。
  • 方法toString: String返回一个包含类名和其元素的字符串表示形式。

2
仅为了完整性(考虑到此答案甚至未提及 tupled),获取所请求的 tupled 函数的方式将类似于:val tupled = (A.apply _).tupled.andThen(_.tupled)。请注意,它采用 两个 元组作为参数(例如,您可以执行:tupled( (123, "foo" ) )( ("bar", 456) ))。将其转换为只接受一个单一元组的函数(在您的情况下为 Tuple4)无法直接实现,除非解构元组,因此在这一点上,最好完全手动编写 tupled 方法。 - Régis Jean-Gilles
2
基本上:case class 的魔法只适用于第一个参数列表。 - Jörg W Mittag
不仅如此,如果构造函数无法通过 eta 扩展隐式转换为普通函数,则伴生对象将不会扩展 FunctionN,并且您将无法在其上获得 tupled 方法。这包括具有多个参数列表的 case 类、泛型 case 类、具有可变参数的 case 类等。 - Régis Jean-Gilles

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接