集合类型内置方法_集合类型

对于学习Scala的Java™开发人员来说,对象提供了自然而轻松的入口点。 在本系列的前几篇文章中,我向您介绍了Scala中的面向对象编程与Java编程没有太大不同的一些方式。 我还向您展示了Scala如何重新审视传统的面向对象的概念,发现它们的不足并在21世纪重新发明它们。 一直以来,重要的事情一直潜伏在幕后,但是等待出现:Scala也是一种功能语言。 (我说是功能性的,而不是所有其他dys功能性语言。)

Scala的功能定位值得探讨,这不仅是因为您用完了所有可用的对象。 Scala中的函数式编程将为您提供一些新的设计结构和构想,以及使某些情况(例如并发性)的编程变得更加容易的内置结构。

本月,您将首次真正地使用Scala进行函数式编程,其中介绍了大多数函数语言共有的四种类型:列表,元组和集合以及Option类型。 您还将了解Scala的数组,这些数组实际上与其他函数语言很陌生。

这些类型中的每一种都提供了一种思考编写代码的新方法。 与传统的面向对象功能结合使用时,它们可以产生惊人的简洁结果。

行使您的选择权

什么时候不是真的不是什么? 为0时,与null相对。

对于我们大多数人都特别了解的概念,事实证明,在软件中尝试表示“无”是非常困难的。 例如,看看C ++社区中有关NULL和0的所有争论,或者SQL社区中有关NULL列值的争论。 NULL或null(如果您愿意)是大多数程序员认为“无”的方式,但这表示Java编程中的一些特殊问题。

考虑一个简单的操作,该操作旨在从内存或磁盘数据库中查找程序员的薪水:API旨在允许调用者传入包含程序员名称的String ,并且它返回……是什么? 从建模的角度来看,它应该返回一个Int ,以传达程序员每年的收入; 但是存在一个棘手的问题,即如果程序员不在数据库中,该如何返回。 (也许她没有被录用,也许她已经被解雇,名字中可能有错字...)如果返回类型为Int ,那么我们就不能返回null ,通常的“标志”表明用户没有在数据库中找不到。 (您可能认为应该引发异常,但是在大多数情况下,数据库中缺少值并不是真正的例外情况,因此此处的异常是不合适的。)

在Java代码中,我们最终将方法标记为返回java.lang.Integer ,这迫使调用者知道该方法可以返回null 。 自然,我们可以依靠程序员来详细记录这种情况,也可以依靠程序员来阅读经过精心准备的文档。 是的...就像我们可以依靠经理彻底听取我们对他们向程序员征收的最后期限的异议,并将这些异议认真地带回他们的管理层和客户一样。

Scala为这种僵局提供了一种通用的功能替代方案。 在某些方面, Option类型或Option[T]违反了描述。 它是一个泛型类,具有恰好两个子类Some[T]None ,用于帮助传达“无值”的可能性,而无需语言类型系统经过认真的回旋来支持该概念。 实际上,如我们将在下一节中所做的那样,使用Option[T]类型应该使事情更加清楚。

使用Option[T] ,关键是要认识到它本质上是大小为“ one”的强类型集合,它使用不同的值None来表示“无”值的可能性。 因此,声明方法为返回Option[T] ,而不是返回null表示没有找到数据的方法,其中T是返回的原始类型。 然后,在找不到数据的情况下,只需返回None ,就像这样:

清单1.您准备好参加足球比赛了吗?
@Test def simpleOptionTest =
  {
    val footballTeamsAFCEast =
      Map("New England" -> "Patriots",
          "New York" -> "Jets",
          "Buffalo" -> "Bills",
          "Miami" -> "Dolphins",
          "Los Angeles" -> null)
    
    assertEquals(footballTeamsAFCEast.get("Miami"), Some("Dolphins"))
    assertEquals(footballTeamsAFCEast.get("Miami").get(), "Dolphins")
    assertEquals(footballTeamsAFCEast.get("Los Angeles"), Some(null))
    assertEquals(footballTeamsAFCEast.get("Sacramento"), None)
  }

请注意,Scala Mapget的返回值不是与传递的键相对应的实际值。 相反,它是一个Option[T]实例,要么是所讨论的值周围的Some() ,要么是None ,这使得在映射中未找到键时很清楚。 如果给定键可以在地图中存在但具有对应的null值(如清单1中的Los Angeles键),则这一点尤其重要。

在大多数情况下,使用Option[T] ,程序员将使用模式匹配 ,这是一种功能强大的概念,可让人们有效地“切换”类型和/或值,更不用说将值绑定到变量了。定义,之间切换Some()None ,并提取出的值Some而不必调用弃用get()方法。 清单2显示了Scala的模式匹配:

清单2.在天堂进行的(模式)匹配
@Test def optionWithPM =
  {
    val footballTeamsAFCEast =
      Map("New England" -> "Patriots",
          "New York" -> "Jets",
          "Buffalo" -> "Bills",
          "Miami" -> "Dolphins")
          
    def show(value : Option[String]) =
    {
      value match
      {
        case Some(x) => x
        case None => "No team found"
      }
    }
    
    assertEquals(show(footballTeamsAFCEast.get("Miami")), "Dolphins")
  }

元组和集合

在C ++中,我们称它们为结构。 在Java编程中,我们称它们为数据传输对象或参数对象。 在Scala中,我们称它们为元组 。 从本质上讲,它们是仅将几个其他数据类型一起收集到单个实例中的类,几乎没有尝试封装或抽象化-实际上,通常没有任何抽象化通常会更有用。

在Scala中创建元组类型非常容易,这很重要:这是要点:如果首先将这些元素公开给公众,那么创建一个描述该类型内部元素的名称就没有任何价值。 考虑清单3:

清单3. tuples.scala
// JUnit test suite
//
class TupleTest
{
  import org.junit._, Assert._
  import java.util.Date
 
  @Test def simpleTuples() =
  {
    val tedsStartingDateWithScala = Date.parse("3/7/2006")

    val tuple = ("Ted", "Scala", tedsStartingDateWithScala)
    
    assertEquals(tuple._1, "Ted")
    assertEquals(tuple._2, "Scala")
    assertEquals(tuple._3, tedsStartingDateWithScala)
  }
}

创建元组很简单,只需将值放在一组括号内,就好像它们在方法调用内一样。 提取值只需要调用_n方法,其中n是感兴趣的元组元素的位置参数: _1代表第一个, _2代表第二个,依此类推。以此类推。传统的Java java.util.Map ,然后,基本上是由两部分组成的元组的集合。

元组使得将多个值作为单个实体携带起来很简单,这意味着元组可以提供否则将在Java编程中非常繁重的工作:多个返回值。 例如,一种方法可以提供数以字符数String ,并返回最热门的人物String ,但如果一个程序员想知道这两个最流行的人物和时代就出现的号码,然后设计得这里比较棘手:要么需要创建一个同时包含字符及其数量的显式类,要么必须将这些值作为字段保存在对象中,并在询问时返回这些字段值。 无论哪种方式,与Scala版本相比,它都将是相当长的代码集。 通过简单地返回包含字符及其关联计数的元组,更不用说简单的“ _1”,“ _ 2”,……访问元组中的每个值,Scala使得返回多个返回值变得容易。

通过简单地返回包含字符及其相关计数的元组,更不用说对元组中每个值的_1轻松访问,Scala使得返回多个返回值变得容易。

正如您将在下一节中看到的那样,Scala程序员经常将Option和元组存储在集合中(例如Array[T]或列表),这在相对简单的结构中为我们提供了极大的灵活性和强大功能。

阵列的阴天的阳光

首先让我们重新访问一个老朋友-数组-现在由Array[T]进行新的管理。 与Java代码中的Array[T]一样,Scala的Array[T]是元素的有序序列,由表示该数组位置的数值索引,并且不得超过该数组的总大小,如清单4所示:

清单4. array.scala
object ArrayExample1
{
  def main(args : Array[String]) : Unit =
  {
    for (i <- 0 to args.length-1)
    {
      System.out.println(args(i))
    }
  }
}

尽管它们等效于Java代码中的数组(毕竟它们是编译成的数组),但是Scala中的数组的定义肯定不同。 对于初学者来说,Scala中的数组实际上是通用类,没有赋予“内置”状态(至少不超过Scala库附带的任何其他类)。 例如,在Scala中,数组被正式定义为Array[T]实例,该类上定义了许多有趣的附加方法,包括无所不在的“长度”方法,该方法毫不奇怪地返回了数组的长度。 因此,在Scala中,可以使用传统意义上的Array ,例如使用从0到args.length - 1Int迭代,并获得数组的第i个元素。 (您可以使用括号而不是方括号来指定要返回的元素,而实际上这是带有有趣名称的另一种方法。)

但这没什么好玩的,也没有乐趣。 (对不起。那里有点编程幽默。对。继续。)

扩展数组

事实证明, Array具有大量方法,它们继承自令人惊讶的丰富父级层次结构: Array扩展Array0 ,扩展ArrayLike[A] ,扩展Mutable[A] ,扩展RandomAccessSeq[A] ,扩展Seq[A]等等,依此类推,等等。 自然,所有这些关联关系意味着Array对它有很多操作,与Java编程相比,它使得在Scala中使用数组更容易。

例如,如清单4所示,可以使用foreach方法以一种(可以说)更简单,(不是可以说)得多的功能方式遍历数组,该方法继承自Iterable特性:

清单5. ArrayExample2
object 
{
  def main(args : Array[String]) : Unit =
  {
    args.foreach( (arg) => System.out.println(arg) )
  }
}

看起来您似乎并没有节省太多,但是将功能(匿名或其他方式)传递到另一个类以在特定语义下执行的能力(在这种情况下,是在整个数组中进行迭代)是功能编程中的一个常见主题。 而且,以这种方式使用高阶函数几乎不限于迭代。 实际上,对数组的内容进行某种过滤过程以剔除不值得的候选者,然后以某种方式处理结果并不少见。 例如,在Scala中,很容易使用filter方法进行过滤,然后获取结果列表并使用map和另一个函数处理每个元素(这次的类型(T) => U ,其中T和U都都是通用类型),或再次foreach 。 我已经尝试了清单6中的后一种方法。(请注意, filter采用(T) : Boolean方法,这意味着它采用数组持有的任何类型的单个参数并返回Boolean 。)

清单6.查找所有Scala程序员
class ArrayTest
{
  import org.junit._, Assert._
  
  @Test def testFilter =
  {
    val programmers = Array(
        new Person("Ted", "Neward", 37, 50000,
          Array("C++", "Java", "Scala", "Groovy", "C#", "F#", "Ruby")),
        new Person("Amanda", "Laucher", 27, 45000,
          Array("C#", "F#", "Java", "Scala")),
        new Person("Luke", "Hoban", 32, 45000,
          Array("C#", "Visual Basic", "F#")),
		new Person("Scott", "Davis", 40, 50000,
		  Array("Java", "Groovy"))
      )

    // Find all the Scala programmers ...
    val scalaProgs =
      programmers.filter((p) => p.skills.contains("Scala") )
    
    // Should only be 2
    assertEquals(2, scalaProgs.length)
    
    // ... now perform an operation on each programmer in the resulting
    // array of Scala programmers (give them a raise, of course!)
    //
    scalaProgs.foreach((p) => p.salary += 5000)
    
    // Should each be increased by 5000 ...
    assertEquals(programmers(0).salary, 50000 + 5000)
    assertEquals(programmers(1).salary, 45000 + 5000)
    
    // ... except for our programmers who don't know Scala
    assertEquals(programmers(2).salary, 45000)
	assertEquals(programmers(3).salary, 50000)
  }
}

map函数将用于创建新Array地方,而原始数组的内容保持不变,这实际上是大多数函数程序员更喜欢的操作方式:

清单7.过滤和映射
@Test def testFilterAndMap =
  {
    val programmers = Array(
        new Person("Ted", "Neward", 37, 50000,
          Array("C++", "Java", "Scala", "C#", "F#", "Ruby")),
        new Person("Amanda", "Laucher", 27, 45000,
          Array("C#", "F#", "Java", "Scala")),
        new Person("Luke", "Hoban", 32, 45000,
          Array("C#", "Visual Basic", "F#"))
		new Person("Scott", "Davis", 40, 50000,
		  Array("Java", "Groovy"))
      )

    // Find all the Scala programmers ...
    val scalaProgs =
      programmers.filter((p) => p.skills.contains("Scala") )
    
    // Should only be 2
    assertEquals(2, scalaProgs.length)
    
    // ... now perform an operation on each programmer in the resulting
    // array of Scala programmers (give them a raise, of course!)
    //
    def raiseTheScalaProgrammer(p : Person) =
    {
      new Person(p.firstName, p.lastName, p.age,
        p.salary + 5000, p.skills)
    }
    val raisedScalaProgs = 
      scalaProgs.map(raiseTheScalaProgrammer)
    
    assertEquals(2, raisedScalaProgs.length)
    assertEquals(50000 + 5000, raisedScalaProgs(0).salary)
    assertEquals(45000 + 5000, raisedScalaProgs(1).salary)
  }

注意,在清单7中, Person的薪水成员可以标记为“ val ,使其不可变”,而不是上面我用来修改各种程序员薪水的var版本。

Scala的Array拥有比我在此处可能列出或演示的方法更多的方法。 作为一般建议,在处理数组时,请积极寻找利用Array提供的方法的方法,而不是使用传统的for ...模式来遍历数组并查找或执行需要查找或完成的操作。 通常,最简单的方法是编写一个执行所需动作的函数(如有必要,嵌套,如清单7中的testFilterAndMap示例),然后将其传递给mapfilterforeach或其他函数之一Array上的方法,具体取决于所需的结果。

列表功能有趣

列表是函数编程多年的核心功能,列表具有与数组在对象空间中多年使用相同的“内置”程度。 列表是构建功能软件的基础,因此,您(作为一名新兴的Scala程序员)必须能够理解它们以及它们如何工作。 即使它们从不考虑新设计,Scala代码也会在整个库中广泛使用列表。 所以学习清单是... ... 势在必行 。

(对不起。更多的函数式编程幽默。不会再发生。)

在Scala中,列表就像数组一样,其核心定义是Scala库List[T]的标准类。 并且,与Array[T]List[T]继承自许多基类和特征,以Seq[T]作为直接基数开始。

从根本上说,列表是可以通过列表的开头或结尾提取的元素的集合。 该列表由Lisp提供,我们的琐事爱好者会记住它是一种主要以“ LISt Processing”为中心的语言的名称,该列表的首位通过car操作获得,尾部通过cdr操作获得。 (名字的原因是历史性的;奖金指向第一个向我发送原因的人。)

实际上,使用列表要比在数组上使用数组要容易得多,这既是因为功能语言在历史上对使用列表(Scala继承了)提供了很好的支持,又因为列表可以很好地构成和分解。 例如,一个函数经常会想要分离列表的内容。 为此,它将选择列表的第一个元素(head)以对该元素进行处理,然后将列表的其余部分递归地再次传递给自己。 这极大地降低了处理代码内部将存在某种共享状态的可能性,并且还使得(如果处理不那么平凡)代码可以拆分为多个线程的可能性更大,因为每个步骤仅需要处理一个元素。

将清单放在一起并将它们拆开非常简单,如清单8所示:

清单8.我们正在...清单
class ListTest
{
  import org.junit._, Assert._
  
  @Test def simpleList =
  {
    val myFirstList = List("Ted", "Amanda", "Luke")
    
    assertEquals(myFirstList.isEmpty, false)
    assertEquals(myFirstList.head, "Ted")
    assertEquals(myFirstList.tail, List("Amanda", "Luke")
    assertEquals(myFirstList.last, "Luke")
  }
}

注意,构造列表与构造数组非常相似。 两者的工作方式都与构造常规对象类似,只是不需要“新”对象。 (这是“案例类”的功能,我们将在以后的文章中进行探讨。)尤其要密切注意tail方法调用的结果-结果不是列表中的最后一个元素(给出了last ),但列表的其余部分不包含第一个元素。

当然,列表的部分功能来自对列表元素的递归处理,这仅意味着从列表中拉出头部,直到列表为空,然后累加结果:

清单9.递归处理
@Test def recurseList =
  {
    val myVIPList = List("Ted", "Amanda", "Luke", "Don", "Martin")
    
    def count(VIPs : List[String]) : Int =
    {
      if (VIPs.isEmpty)
        0
      else
        count(VIPs.tail) + 1
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

请注意,如果我不考虑count的返回类型,Scala编译器或解释器将变得脾气暴躁-因为这是一个尾递归调用,该优化旨在减少在大量递归操作中创建的堆栈帧的数量,它需要指定其返回类型。 当然,仅使用List的“ length”成员来获取List中的项目数肯定会更容易,但是重点是说明“大”列表处理的强大功能。 清单9中的整个方法是完全线程安全的。 我可以肯定,因为在其处理中使用的整个中间状态都保存在堆栈中的参数中,因此,根据定义,不能由多个线程访问。 函数方法的妙处在于,实际上很难进行函数编程并仍然创建共享状态。

列出API

列表具有一些其他有趣的属性,例如使用::方法构造列表的其他方法。 (是的,方法。伙计,这是另一个带有有趣名字的方法。在这里什么也看不见,继续前进。)因此,与其使用“ List构造函数”语法构造列表,还可以“约束”它们(作为双冒号方法)被称为),就像这样:

清单10.我认为:: == C ++?
@Test def recurseConsedList =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    def count(VIPs : List[String]) : Int =
    {
      if (VIPs.isEmpty)
        0
      else
        count(VIPs.tail) + 1
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

使用::方法时要小心-它引入了一些有趣的规则。 这种语法在功能语言中非常常见,以至于Scala的创建者选择支持它,但是为了使语法正常正常地工作,必须引入一个古怪的规则:任何以冒号结尾的“带有有趣名称的方法”是右关联的 ,这意味着整个表达式从表达式的最右边开始,以Nil开头,而Nil恰好是List 。 因此,这意味着::可以被确定为全局::方法,而不是(在这种情况下) String的成员方法; 反过来,这意味着您可以构建所有内容的列表。 使用:: ,最右边的元素必须是列表,否则您将收到错误消息。

在Scala中使用列表的最强大方法之一是与模式匹配相结合,这是由于列表不仅可以在类型和值上进行匹配,而且还可以同时进行变量绑定。 例如,我可以使用模式匹配来简化清单10中的列表代码,以实现其中至少包含一个元素和一个为空的列表:

清单11.列表中的模式匹配
@Test def recurseWithPM =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    def count(VIPs : List[String]) : Int =
    {
      VIPs match
      {
        case h :: t => count(t) + 1
        case Nil => 0
      }
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

在第一种case ,列表的开头将被剔除并绑定到变量h ,而其余​​部分(尾部)将绑定到t ; 在这种情况下, h不会执行任何操作(实际上,最好用通配符_代替h来表示永远不会使用head,以表明它是永远不会使用的变量的占位符)。 但是,与前面的示例一样,递归地传递了t来再次count 。 还要记住,Scala中的每个表达式都隐式地返回一个值。 在这种情况下,模式匹配表达式的结果将是递归调用count + 1 ,或者当我们到达列表末尾时返回0

假设两者的权衡大约是相同的代码行数,那么使用模式匹配的价值在哪里? 确实,对于这种简单的东西,很难找到价值。 但是对于某些稍微复杂的事情,例如扩展示例以沿途捕获特定值,您可能会重新考虑。

清单12.模式匹配器,模式匹配器,请给我Amanda!
@Test def recurseWithPMAndSayHi =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    var foundAmanda = false
    def count(VIPs : List[String]) : Int =
    {
      VIPs match
      {
        case "Amanda" :: t =>
          System.out.println("Hey, Amanda!"); foundAmanda = true; count(t) + 1
        case h :: t =>
          count(t) + 1
        case Nil =>
          0
      }
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
    assertTrue(foundAmanda)
  }

不久之后,真正复杂的示例(尤其是在正则表达式或XML节点领域)就开始大举支持模式匹配方法,这花了很长时间。 模式匹配也不仅限于列表; 没有理由不能将它从以前扩展到数组示例。 实际上,这是上面recurseWithPMAndSayHi测试的数组示例:

清单13.模式匹配器,模式匹配器,为我匹配一个数组
@Test def recurseWithPMAndSayHi =
  {
    val myVIPList = Array("Ted", "Amanda", "Luke", "Don", "Martin")

    var foundAmanda = false
    
    myVIPList.foreach((s) =>
      s match
      {
        case "Amanda" =>
          System.out.println("Hey, Amanda!")
          foundAmanda = true
        case _ =>
          ; // Do nothing
      }
    )

    assertTrue(foundAmanda)
  }

如果您想做一个练习,请尝试构建清单13的递归版本,该递归版本也可以在recurseWithPMAndSayHi范围内声明recurseWithPMAndSayHi变的var情况下进行计数。 提示:可能需要多个模式匹配块。 (本文的代码下载包含一个解决方案-但我希望您在偷看之前先尝试一下。)

结论

Scala丰富的收藏集(对双关语)是其某些功能历史和功能集的直接结果。 元组提供了一种收集一组松散绑定的值的简便方法; Option[T]提供了一种简单的方法来以简单的方式指示“某些”值,而不是“否”值。 数组通过一些增强的功能提供对传统Java样式的数组语义的访问; 列表是功能语言的主要集合,依此类推。

但是,请谨慎使用其中的某些功能,尤其是元组:容易陷入使用它们而忘记传统的基本对象建模,而直接使用元组的情况。 如果某个特定的元组(例如名称,年龄,薪水和已知的编程语言列表)例行出现在代码库中,请继续将其建模为正式的类类型和对象。

关于Scala的美丽之处在于它既是功能性的又是面向对象的,因此即使您学习使用Scala的功能类型,也可以保持传统的课堂设计眼光。


翻译自: https://www.ibm.com/developerworks/java/library/j-scala06278/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值