【问题标题】:Is there a way to have tuples with named fields in Scala, similar to anonymous classes in C#?有没有办法在 Scala 中拥有具有命名字段的元组,类似于 C# 中的匿名类?
【发布时间】:2009-12-01 17:25:31
【问题描述】:

见:Can I specify a meaningful name for an anonymous class in C#?

在 C# 中你可以这样写:

var e = new { ID = 5, Name= "Prashant" };
assertEquals( 5, e.ID )

但在 Scala 中我最终会写作:

var e = (5, "Prashant")
assertEquals( 5, e._1 )

Scala 通过使用泛型来维护类型安全(与 C# 一样),但失去了每个字段名称的可读性,例如我使用“_1”而不是“ID”。

Scala 中有这样的东西吗?

【问题讨论】:

    标签: c# scala tuples


    【解决方案1】:
    object T {
      def main(args: Array[String]) {  
        val e = new { var id = 5; var name = "Prashant" }
        assert(e.id == 5)
      }
    }
    

    好的,让我们把事情说清楚。这确实在 Scala 2.7 和 Scala 2.8 上使用了反射,因为在这种情况下,e 的类型是 结构类型,Scala 通过反射进行处理。这是清理时生成的代码 (scalac -Xprint:cleanup):

    package <empty> {
      final class T extends java.lang.Object with ScalaObject {
        private <synthetic> <static> var reflMethod$Cache1: java.lang.reflect.Method = null;
        private <synthetic> <static> var reflClass$Cache1: java.lang.Class = null;
        <synthetic> <static> def reflMethod$Method1(x$1: java.lang.Class): java.lang.reflect.Method = {
          if (T.this.reflMethod$Cache1.eq(null).||(T.this.reflClass$Cache1.ne(x$1)))
            {
              T.this.reflMethod$Cache1 = x$1.getMethod("id", Array[java.lang.Class]{});
              T.this.reflClass$Cache1 = x$1;
              ()
            };
          T.this.reflMethod$Cache1
        };
        @remote def $tag(): Int = scala.ScalaObject$class.$tag(T.this);
        def main(args: Array[java.lang.String]): Unit = {
          val e: java.lang.Object = {
            new T$$anon$1()
          };
          scala.this.Predef.assert(scala.Int.unbox({
            var exceptionResult1: java.lang.Object = _;
            try {
              exceptionResult1 = T.reflMethod$Method1(e.getClass()).invoke(e, Array[java.lang.Object]{})
            } catch {
              case ($1$ @ (_: java.lang.reflect.InvocationTargetException)) => {
                exceptionResult1 = throw $1$.getCause()
              }
            };
            exceptionResult1
          }.$asInstanceOf[java.lang.Integer]()).==(5))
        };
        def this(): object T = {
          T.super.this();
          ()
        }
      };
      final class T$$anon$1 extends java.lang.Object {
        private[this] var id: Int = _;
        <accessor> def id(): Int = T$$anon$1.this.id;
        <accessor> def id_=(x$1: Int): Unit = T$$anon$1.this.id = x$1;
        private[this] var name: java.lang.String = _;
        <accessor> def name(): java.lang.String = T$$anon$1.this.name;
        <accessor> def name_=(x$1: java.lang.String): Unit = T$$anon$1.this.name = x$1;
        def this(): T$$anon$1 = {
          T$$anon$1.this.id = 5;
          T$$anon$1.this.name = "Prashant";
          T$$anon$1.super.this();
          ()
        }
      }
    }
    

    正在进行一些缓存,但如果我在idname 之间交替使用,它就会使缓存失效。 Scala 2.8 也进行反射和缓存,但它使用更高效的缓存技术,应该提供更好的整体性能。作为参考,这里是 Scala 2.8 的清理:

    package <empty> {
      final class T extends java.lang.Object with ScalaObject {
        final private <synthetic> <static> var reflParams$Cache1: Array[java.lang.Class] = Array[java.lang.Class]{};
        @volatile
        private <synthetic> <static> var reflPoly$Cache1: scala.runtime.MethodCache = new scala.runtime.EmptyMethodCache();
        <synthetic> <static> def reflMethod$Method1(x$1: java.lang.Class): java.lang.reflect.Method = {
          var method1: java.lang.reflect.Method = T.reflPoly$Cache1.find(x$1);
          if (method1.ne(null))
            return method1
          else
            {
              method1 = x$1.getMethod("id", T.reflParams$Cache1);
              T.reflPoly$Cache1 = T.reflPoly$Cache1.add(x$1, method1);
              return method1
            }
        };
        def main(args: Array[java.lang.String]): Unit = {
          val e: java.lang.Object = {
            new T$$anon$1()
          };
          scala.this.Predef.assert(scala.Int.unbox({
            val qual1: java.lang.Object = e;
            {
              var exceptionResult1: java.lang.Object = _;
              try {
                exceptionResult1 = T.reflMethod$Method1(qual1.getClass()).invoke(qual1, Array[java.lang.Object]{})
              } catch {
                case ($1$ @ (_: java.lang.reflect.InvocationTargetException)) => {
                  exceptionResult1 = throw $1$.getCause()
                }
              };
              exceptionResult1
            }.$asInstanceOf[java.lang.Integer]()
          }).==(5))
        };
        def this(): object T = {
          T.reflParams$Cache1 = Array[java.lang.Class]{};
          T.reflPoly$Cache1 = new scala.runtime.EmptyMethodCache();
          T.super.this();
          ()
        }
      };
      final class T$$anon$1 extends java.lang.Object {
        private[this] var id: Int = _;
        <accessor> def id(): Int = T$$anon$1.this.id;
        <accessor> def id_=(x$1: Int): Unit = T$$anon$1.this.id = x$1;
        private[this] var name: java.lang.String = _;
        <accessor> def name(): java.lang.String = T$$anon$1.this.name;
        <accessor> def name_=(x$1: java.lang.String): Unit = T$$anon$1.this.name = x$1;
        def this(): T$$anon$1 = {
          T$$anon$1.super.this();
          T$$anon$1.this.id = 5;
          T$$anon$1.this.name = "Prashant";
          ()
        }
      }
    }
    

    【讨论】:

    • 聪明。这是否会使用这些 id 和 name 成员创建一个新的匿名 AnyRef 子类?
    • 是的,确实如此。它在 Scala 2.7,IIRC 中表现不佳,因为它会使用反射。我不确定,但如果这很重要,你应该检查一下。
    • 我不知道为什么会这样。编译器可以访问(匿名)类定义,因此不需要反射。据我所知,只有结构类型需要使用反射。
    • 有趣..什么时候会用到反射? 2.8 会避免反射的需要吗?
    • 在 Scala 中,匿名类是类型安全的,并在编译时进行检查。这里的语言设计者决定不污染事物,方法是为使用结构类型作为参数时可以传递的每种对象类型创建一个带有命名类的新 thunk。我不知道他们会继续这样做多久——我会想象当 JDK 7 发布时会在底层重构 Scala 编译器,其中有几个新的 VM 特性会加速这种事情,当他们看到Java 7 如何处理闭包。
    【解决方案2】:

    您还可以命名要分配的元组的各个部分,如下所示:

    val (ID, Name) = (5, "Prashant")
    assertEquals( 5, ID )
    

    你也可以这样使用:

    val (ID, Name, Age) = functionThatReturnsATuple3
    println("ID: " + ID + ", age: " + Age)
    

    当我第一次阅读_x 语法时,我认为它很棒并且经常使用它。从那以后,我基本上不再使用它,因为当我不得不查看两个月前编写的代码时,我不得不花费大量时间来弄清楚_1_2 等的类型是什么。我想事后看来很明显idpair._1 更具可读性。

    这也可以在mapfilter等函数内部使用,例如:

    val list: List[ (Int, String, Double) ] = ...
    list map { case (id, name, time) => ... }
    list filter { case (_, name, _) => name == "X" }
    

    请注意,在filter 中,您可以将_s 用于您不会在函数主体中使用的元素。这在浏览此类代码以确定正在使用结构的哪些部分以及如何建立值时很有用。

    【讨论】:

    • 这看起来很有趣 - 但你能用它来投影吗,例如将项目列表映射到命名元组?
    • 我不太清楚你的意思,你可以这样做: val list: List[ (Int, String, Double) ] = ...; list map { case (id, name, time) => ... }
    • 在 C# 中你可以这样做:employees.map( e => new { FirstName = e.Name.First, LastName = e.Name.Last } ).toList 例如,哪个“项目”将员工列表转换为匿名名称类列表。所以在 Scala 中你可以这样做:employees.map( e => ((e.Name.First, e.Name.Last)) ).toList 但你会丢失元组中字段的名称。
    • 事实上,在 C# 中(你可能已经厌倦了我知道的 C# cmets)你可以做 employees.map( e => new { e.Name.First, e.Name.Last } ).toList 将使用匿名类,其字段名称为 First 和 Last 自动。
    • 这非常好,但我认为在大量使用标准集合库时用处不大,除非您将大多数集合转换结果映射到对象中,这违背了连贯且没有样板的目的.还是这样?
    【解决方案3】:

    我会简单地创建一个案例类:

    object Yo{
      case class E(id: Int, name: String)
    
      def main(){
        val e = E(5,"Prashant")
        println("name="+e.name+", id="+e.id)
      }
    }
    

    不确定它是否和Daniel's answer 一样高效,但我希望它是一样的(我很感激 cmets)。无论如何,我发现它更具可读性,如果E 有多个实例,则只使用一个共享的附加行。你也可以在案例类中添加方法,例如:

    case class E(id: Int, name: String){
      def hasId(id: Int) = this.id==id
    }
    
    val e = E(5,"Prashant")
    assert(e hasId 5)
    

    【讨论】:

    • 这更具可读性,但代码变得繁琐且可读性越差,您在生成和使用各种元组的大量集合转换中越多。对于这些情况,我认为此处相邻答案之一中的匿名对象可能效果最好。
    • 这是一个偏好问题。请注意,我遵循(因为我同意)originate.com/library/scala-guide-best-practices 第 7 点:“不要过度使用元组”。
    • 我认为您应该能够对命名元组的集合进行排序,但是不能对案例类进行自动排序,因为字段没有排序(而它们是在元组中排序的)。跨度>
    • 我认为案例类字段是有序的,但不提供通过索引访问的 api。请注意,据我所知,元组是相同的,唯一的区别是归档的 names 是由连续数字(_1,_2,...)组成的。但是您不能使用 Int 访问元组项,例如 myTuple(2)(至少不使用一些无形的黑暗魔法 :-)
    【解决方案4】:

    正如 Juh_ 所建议的,扩展一个案例类应该做你想做的事:

    scala> case class E(id: Int, name: String)
    defined class E
    
    scala> val e = new E(5, "Prashant")
    e: E = E(5,Prashant)
    
    scala> e.name
    res3: String = Prashant
    

    Case 类提供了一个 equals 方法,它们还扩展了 Product 特征,这与 Scala 元组扩展的特征相同。也许将来他们也会extend the ProductN traits

    如果您按照其他答案中的建议使用匿名类,那么您最终不会得到真正的元组!比如你没有得到equals方法:

    scala> val x = new { val count = 5 }
    x: AnyRef{val count: Int} = $anon$1@29ca901e
    
    scala> val y = new { val count = 5 }
    y: AnyRef{val count: Int} = $anon$1@1dfe2924
    
    scala> x == y
    res4: Boolean = false
    

    从 Scala 2.11 开始,扩展 Tuple2 确实有效,但已弃用,因为您不应该扩展案例类。

    您也可以扩展 Product2 特征,但它不提供任何方法的实现,因此您必须自己编写所有方法。

    您可能还可以使用 Shapeless HList,它会以添加外部依赖项为代价为您提供许多精美的功能。

    我还尝试了 Twitter 的 jaqen 库,但在 Scala 2.11 中无法为我编译。

    我目前使用的是 Scala 2.11,因此我不能保证此建议适用于其他版本的 Scala。

    【讨论】:

      【解决方案5】:

      Scala 2.8 改进了类型系统,使得可以拥有静态和异构类型的数组和列表,因此大概可以对映射进行相同的操作。查看Jesper Nordenberg's blog on "Type Lists and Heterogeneously Typed Arrays" 了解他的实现。

      【讨论】:

      • 您能详细说明一下吗?查看 Jesper 的帖子,它似乎只是在 scala 中实现元组的另一种方式,但不必拥有 Tuple1、Tuple2、Tuple3 等。我还没有看到它如何解决元组中字段的名称。
      • "...大概地图也可以这样做。"当然,留给读者作为练习。无论如何,如果在创建实例时字段是固定的,那么 Daniel 的解决方案更可取。
      • 通过“完成地图”,您的意思是有一个地图,其中每个项目都是不同的类型,并且字段名称用作键?
      • 我也是这么想的,但没想太多。
      猜你喜欢
      • 2011-02-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-22
      相关资源
      最近更新 更多