Ohhnews

分类导航

$ cd ..
foojay原文

BoxLang 1.14.0: BoxSet来了,BoxLang的一等集合类型

#boxlang#boxset#集合类型#数据处理#jvm语言

目录

为什么要用集合?先看问题认识 BoxSet创建集合:所有方式

[LOADING...]

BoxLang 1.14.0 附带了一个新的动态一流 Set 类型,直接内置于语言中。这不是一个需要手动调用的包装器,也不是多年前从 Stack Overflow 答案中粘贴的 createObject( "java", "java.util.HashSet" ) 咒语。这是一个真正的 BoxSet,具有字面量语法、运算符重载、完整的函数式管道、变更监听器、JSON 序列化和深度 Java 互操作。

如果你曾经用循环对数组进行去重、逐个元素比较两个集合,或者基于结构体建模权限系统——那么 Set 就是你一直缺少的工具。让我们深入探讨。

为什么要用集合?先看问题

数组是有序、有索引且允许重复的。结构体是键值映射。两者都是基础,但都没有建模现实世界中最常见的形状之一:一袋唯一的东西。

想一想:

  • 用户的已分配角色:[ "admin", "editor", "admin" ]——这个重复是一个潜在的 bug
  • 博客文章上的标签——顺序通常不重要,唯一性才是关键
  • 活动特性开关——成员检测是你需要的唯一操作
  • 需要比较的两个数据集——什么是新的、什么消失了、什么共享了?
  • 需要知道所有传入参数的精确顺序和内容?
  • 爬虫中的 URL 去重
  • 权限交集:这个用户在这个资源上能做什么?

在 BoxSet 出现之前,你需要用数组(慢的 arrayContains 查找、手动去重循环)或结构体(把键当作值,序列化别扭)来近似所有这些场景。两者都是变通方案。BoxSet 才是真正的答案。

认识 BoxSet

BoxSet 是一个一流的 BoxLang 类型,它在 java.util.Set 基础上进行了包装,并实现了完整的语言集成。底层会根据你的需求选择三种 Java 后端实现之一:

BoxSet 类型Java 后端特性
default / hashHashSet无顺序,查找最快
linked / orderedLinkedHashSet保持插入顺序
sorted / treeTreeSet始终按自然升序排列

每种变体都会自动强制唯一性。每种变体都支持完整的成员函数 API、运算符重载和函数式管道。从第三方库获得的 Java Set 对象可以直接插入——BoxLang 会包装它们而不进行复制。

创建集合:所有方式

BoxLang 提供了几种符合人体工程学的创建路径,取决于你的上下文。

setNew() —— 主力函数

// 空的默认(哈希)Set —— 最快,无顺序
s = setNew()

// 创建时预填充
s = setNew( values=[ "alpha", "beta", "gamma", "alpha" ] )
s.size()    // 3 —— 重复项自动丢弃

// Linked:保持插入顺序
s = setNew( type="linked", values=[ "c", "a", "b" ] )
s.toArray()    // ["c", "a", "b"] —— 顺序保留

// Sorted:始终自然升序
s = setNew( type="sorted", values=[ 9, 1, 5, 3 ] )
s.toArray()    // [1, 3, 5, 9]

// 区分大小写:将 "Hello" 和 "hello" 视为不同的值
s = setNew( values=[ "Hello", "hello", "HELLO" ], caseSensitive=true )
s.size()    // 3

setOf() —— 可变参数简写

当你预先知道你的值时,setOf() 是最简洁的表达:

roles = setOf( "admin", "editor", "viewer" )
primes = setOf( 2, 3, 5, 7, 11, 13 )

参数列表中的重复项会被静默丢弃——没有错误,没有麻烦。

字面量语法:set{ ... }

BoxLang 1.14.0 引入了一种集合字面量语法,读起来就像概念本身:

// 内联字面量
permissions = set{ "read", "write", "execute" }

// 空集合
empty = set{}

// 直接将数组展开到集合字面量中
extra = [ 4, 5, 6 ]
merged = set{ 1, 2, 3, ...extra }
// 结果:{1, 2, 3, 4, 5, 6}

// 展开另一个集合
defaults = set{ "read", "write" }
full = set{ "execute", ...defaults }

// 展开一个范围——特别优雅
digits = set{ ...(0..9) }
// 结果:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

"set" 关键字由解析器守卫,因此不会与名为 set 的变量冲突。你现有的代码是安全的。

从其他类型转换

// 数组转 Set —— 免费去重
tags = [ "boxlang", "jvm", "boxlang", "oss" ].toSet()
tags.size()    // 3

// 在去重的同时保持插入顺序
orderedTags = [ "c", "a", "b", "a" ].toSet( "linked" )

// 从数组排序
sorted = [ 9, 1, 5, 3 ].toSet( "sorted" )

// 直接将分隔字符串拆分为 Set
csv = "admin,editor,viewer,admin".listToSet()
csv.size()    // 3

// 自定义分隔符
pipe = "read|write|execute|read".listToSet( delimiter="|", type="linked" )

// 从查询中 —— 一次性获取去重的列值
q = queryNew( "name,dept", "varchar,varchar", [
    [ "Alice", "Engineering" ],
    [ "Bob", "Marketing" ],
    [ "Carol", "Engineering" ]
] )
depts = q.columnData( "dept" ).toSet()
// 结果:{"Engineering", "Marketing"}

三种变体实战对照

选择正确的变体关乎正确性和性能。

默认(HashSet)—— 不关心顺序时

// 权限检查:顺序无关紧要,成员速度才是关键
userRoles  = setOf( "editor", "viewer", "moderator" )
adminRoles = setOf( "admin", "superadmin" )

canAdmin = userRoles.some( r -> adminRoles.contains( r ) )
// false

// 特性开关 —— 无论开关数量多少,恒定时间查找
activeFlags = setNew( values=queryGetColumn( flagQuery, "flagName" ) )
if ( activeFlags.contains( "dark_mode_v2" ) ) {
    // 渲染深色模式
}

有序(LinkedHashSet)—— 需要保持插入顺序时

// 面包屑轨迹 —— 按顺序访问的页面,不重复
trail = setNew( type="linked" )
trail.add( "/home" )
trail.add( "/products" )
trail.add( "/products/123" )
trail.add( "/home" )          // 已经存在,静默忽略
trail.toArray()
// ["/home", "/products", "/products/123"]

// 处理管道阶段 —— 有序,去重
pipeline = setNew( type="linked", values=[ "validate", "enrich", "normalize", "validate" ] )
for ( stage in pipeline ) {
    runStage( stage )    // validate 只运行一次
}

排序(TreeSet)—— 始终需要自然顺序时

// 版本号(当作字符串处理)的优先级队列
versions = setNew( type="sorted", values=[ "1.14.0", "1.9.0", "2.0.0", "1.10.0" ] )
versions.toArray()
// ["1.10.0", "1.14.0", "1.9.0", "2.0.0"] —— 按字典序,注意你的版本方案

// 整数范围 —— 总是排序
scores = setNew( type="sorted" )
scores.addAll( [ 87, 42, 99, 55, 87, 42 ] )
scores.toArray()
// [42, 55, 87, 99]

// 通过 toArray() 廉价获取最小值和最大值
arr = scores.toArray()
writeDump( "Low: #arr[ 1 ]#, High: #arr[ arr.len() ]#" )

成员检测与迭代

检测成员

granted = setOf( "read", "write", "execute" )

granted.contains( "read" )     // true
granted.has( "delete" )        // false(contains 的别名)

// 同时检测多个
granted.containsAll( [ "read", "write" ] )    // true
granted.containsAll( [ "read", "sudo" ] )     // false

迭代

tags = setNew( type="linked", values=[ "boxlang", "jvm", "oss" ] )

// for-in 循环
for ( tag in tags ) {
    println( tag )
}

// each() —— 在函数式管道中更简洁
tags.each( tag => {
    processTag( tag )
} )

集合代数:真正的力量

这是 BoxSet 的价值所在。四种代数运算,既可以作为成员方法使用,也可以通过重载运算符使用。

并集 —— 两个集合的所有唯一元素

backendSkills  = setOf( "java", "sql", "boxlang", "redis" )
frontendSkills = setOf( "javascript", "css", "boxlang", "react" )

allSkills = backendSkills.union( frontendSkills )
// {java, sql, boxlang, redis, javascript, css, react}

// 运算符语法:+
allSkills = backendSkills + frontendSkills

交集 —— 两个集合共有的元素

teamA = setOf( "alice", "bob", "carol", "dan" )
teamB = setOf( "bob", "carol", "eve" )

sharedMembers = teamA.intersection( teamB )
// {bob, carol}

// 运算符语法:*
sharedMembers = teamA * teamB

差集 —— 在 A 中但不在 B 中的元素

allUsers     = setOf( "alice", "bob", "carol", "dan", "eve" )
activeUsers  = setOf( "alice", "carol", "eve" )

inactiveUsers = allUsers.difference( activeUsers )
// {bob, dan}

// 运算符语法:-
inactiveUsers = allUsers - activeUsers

对称差集 —— 在两者之一但不同时存在的元素

lastWeekUsers = setOf( "alice", "bob", "carol" )
thisWeekUsers = setOf( "bob", "carol", "dan", "eve" )

// 谁加入或离开了?
changed = lastWeekUsers.symmetricDifference( thisWeekUsers )
// {alice, dan, eve}

// 运算符语法:^
changed = lastWeekUsers ^ thisWeekUsers

运算符接受"松散"右操作数

你不需要先将所有内容转换为 Set:

base = setOf( 1, 2, 3, 4, 5 )

result = base + [ 6, 7 ]         // Set + Array
result = base * "3,4,5,6"        // Set * 逗号分隔字符串
result = base - (1..2)           // Set - Range

// 复合赋值运算符
base -= set{ 1, 2 }
// base 现在为 {3, 4, 5}

base *= [ 3, 4 ]
// base 现在为 {3, 4}

当两边的操作数都不是 Set 时,运算符会回退到标准数学运算:2 + 3 = 54 ** 3 = 122 ^ 3 = 8。你现有的算术代码是安全的。

函数式编程管道

BoxSet 配备了与你从 Arrays 中了解的相同的函数式词汇表。每个操作都返回一个新的 Set(或一个标量),原始 Set 保持不变。

scores = setNew( type="sorted", values=[ 55, 72, 88, 91, 43, 88, 100 ] )
// 存储为:{43, 55, 72, 88, 91, 100}

// map —— 转换每个元素,返回一个新的 Set
bonusScores = scores.map( s -> s + 5 )
// {48, 60, 77, 93, 96, 105}

// filter —— 保留匹配的元素
passing = scores.filter( s -> s >= 60 )
// {72, 88, 91, 100}

// reject —— filter 的反操作
failing = scores.reject( s -> s >= 60 )
// {43, 55}

// reduce —— 折叠为单个值
total = scores.reduce( ( acc, s ) => acc + s, 0 )
// 449

average = total / scores.size()
// 74.83...

// every —— 所有元素是否都满足谓词?
allPassing = scores.every( s -> s >= 60 )
// false

// some —— 至少有一个元素满足谓词?
hasA = scores.some( s -> s >= 90 )
// true

// none —— 没有任何元素满足谓词?
noneNegative = scores.none( s -> s < 0 )
// true

// find —— 第一个匹配谓词的元素
firstHigh = scores.find( s -> s >= 90 )
// 91(或 100,对于 HashSet 迭代顺序不保证 —— 使用 sorted/linked 以获得可预测性)

链式调用自然工作:

result = setOf( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 )
    .filter( n -> n % 2 == 0 )    // 偶数:{2, 4, 6, 8, 10}
    .map( n -> n * n )            // 平方:{4, 16, 36, 64, 100}
    .reduce( ( acc, n ) => acc + n, 0 )
// 220
```## 真实场景示例

### 1. 基于角色的权限控制

class PermissionService {

function canAccess( required userRoles, required string resource ) {
    resourcePermissions = getResourcePermissions( resource )

    // 用户的哪些角色拥有访问权限?
    return !userRoles.intersection( resourcePermissions ).isEmpty()
}

function mergeRoles( required userRoles, required groupRoles ) {
    // 用户拥有个人角色与组角色的并集
    return userRoles.union( groupRoles )
}

function getEffectiveDenials( required userRoles, required deniedRoles ) {
    // 用户拥有且在该资源上被明确拒绝的角色
    return userRoles.intersection( deniedRoles )
}

private function getResourcePermissions( required string resource ) {
    return {
        "/admin"   : setOf( "admin", "superadmin" ),
        "/reports" : setOf( "admin", "analyst", "manager" ),
        "/api"     : setOf( "developer", "admin" )
    }[ resource ] ?: setNew()
}

}

svc = new PermissionService()

userRoles = setOf( "editor", "analyst", "viewer" ) groupRoles = setOf( "manager", "viewer" )

effective = svc.mergeRoles( userRoles, groupRoles ) // {editor, analyst, viewer, manager}

canSeeReports = svc.canAccess( effective, "/reports" ) // true(analyst 或 manager 匹配)

canSeeAdmin = svc.canAccess( effective, "/admin" ) // false


### 2. 标签去重与分类交集

// 内容标签系统 post1Tags = setNew( type="linked", values=[ "boxlang", "jvm", "performance", "oss" ] ) post2Tags = setNew( type="linked", values=[ "jvm", "java", "boxlang", "interop" ] ) post3Tags = setNew( type="linked", values=[ "performance", "caching", "redis", "oss" ] )

// 出现在多篇文章中的标签 commonTagsP1P2 = post1Tags * post2Tags // {boxlang, jvm}

// 内容库中的所有唯一标签 allTags = post1Tags + post2Tags + post3Tags // {boxlang, jvm, performance, oss, java, interop, caching, redis}

// post1 独有的标签——适合“独家”徽章 exclusiveToPost1 = post1Tags - post2Tags - post3Tags // {boxlang}——只有“boxlang”出现在p1而不在另外两篇中……实际上结果因情况而异

// 查找与 post1 共享至少2个标签的文章(相关文章) relatedThreshold = 2 isRelated = ( post1Tags * post2Tags ).size() >= relatedThreshold // true


### 3. 数据集变化检测

一种常见的 ETL 模式:两个数据快照之间发生了什么变化?

function detectChanges( required previousIds, required currentIds ) { prevSet = previousIds.toSet() currSet = currentIds.toSet()

return {
    "added"     : currSet - prevSet,     // 自上次运行后新增
    "removed"   : prevSet - currSet,     // 自上次运行后移除
    "unchanged" : prevSet * currSet,     // 两者都有
    "changed"   : prevSet ^ currSet      // 发生变动的任何元素
}

}

yesterdayUsers = queryGetColumn( getYesterdayQuery(), "user_id" ) todayUsers = queryGetColumn( getTodayQuery(), "user_id" )

diff = detectChanges( yesterdayUsers, todayUsers )

writeDump( "今日新增注册: #diff.added.size()#" ) writeDump( "流失用户: #diff.removed.size()#" )

// 只向真正的新用户发送欢迎邮件 diff.added.each( userId => { emailService.sendWelcome( userId ) } )


### 4. 函数式链式调用的 URL 去重管道

class CrawlerPipeline {

property name="visited"   type="Set"
property name="queued"    type="Set"
property name="blacklist" type="Set"

function init() {
    variables.visited   = setNew( type="linked" )
    variables.queued    = setNew( type="linked" )
    variables.blacklist = setOf( "login", "logout", "admin" )
    return this
}

function enqueue( required array urls ) {
    // 标准化、拒绝黑名单路径、排除已访问
    fresh = urls
        .toSet( "linked" )                                // 去重
        .filter( url -> !isBlacklisted( url ) )          // 丢弃黑名单
        .difference( variables.visited )                  // 丢弃已见
        .difference( variables.queued )                   // 丢弃已排队

    variables.queued.addAll( fresh.toArray() )
    return fresh.size()
}

function processNext() {
    if ( variables.queued.isEmpty() ) return

    // LinkedHashSet 通过 toArray()[1] 提供 FIFO 顺序
    target  = variables.queued.toArray().first()
    variables.queued.remove( target )
    variables.visited.add( target )

    return crawl( target )
}

function stats() {
    return {
        "visited" : variables.visited.size(),
        "queued"  : variables.queued.size(),
        "overlap" : ( variables.visited * variables.queued ).size()    // 应始终为0
    }
}

private function isBlacklisted( required string target ) {
    return variables.blacklist.some( b -> target.findNoCase( b ) > 0 )
}

}


## 大小写敏感与数值规范化

默认情况下,BoxSet 对字符串**不区分大小写**,与 BoxLang 的一般动态语义一致:

s = setNew( values=[ "Hello", "hello", "HELLO", "hElLo" ] ) s.size() // 1

s.contains( "HELLO" ) // true s.contains( "hElLo" ) // true


当需要精确大小写的唯一性时,可以选择启用大小写敏感:

tokens = setNew( caseSensitive=true, values=[ "Bearer", "bearer", "BEARER" ] ) tokens.size() // 3

tokens.contains( "bearer" ) // true tokens.contains( "Bearer" ) // true tokens.contains( "beareR" ) // false


数值规范化与大小写敏感无关。`1`、`1L`、`1.0` 和 `1.00` 在集合中始终被视为相同的值:

nums = setNew( values=[ 1, 1.0, 1L, 1.00 ] ) nums.size() // 1


## Java 互操作

由于 `BoxSet` 包装了 `java.util.Set`,双向集成十分顺畅。

### 包装已有的 Java Set

import java.util.HashSet

javaSet = createObject( "java", "java.util.HashSet" ).init() javaSet.add( "a" ) javaSet.add( "b" )

// 包装——不复制,修改双向传递 bxSet = javaSet castAs "Set" bxSet.add( "c" )

javaSet.contains( "c" ) // true——同一底层对象 bxSet.size() // 3


### Struct 的键集与值集

config = { "host" : "localhost", "port" : 5432, "ssl" : true, "database" : "myapp" }

// 键作为集合 keys = config.keySet() keys.contains( "port" ) // true

// 值作为集合(已去重) values = config.valueSet()

// 用于检查是否有任何键与禁止列表重叠 forbidden = setOf( "password", "secret", "token", "key" ) hasSensitiveKeys = !keys.isDisjointFrom( forbidden ) // false——我们的键都不在禁止列表中


任何返回 `java.util.Set` 的 Java 库方法——例如 Spring Security 的授权权限、JPA 查询结果、Guava 的 ImmutableSet——都可以直接与 BoxLang 的集合 BIF 和成员函数配合使用。

## 不可变集合

当你需要不可变契约时——配置、常量、查找表:

ALLOWED_METHODS = setOf( "GET", "POST", "PUT", "DELETE", "PATCH" ).toUnmodifiable()

ALLOWED_METHODS.size() // 5 ALLOWED_METHODS.contains( "GET" ) // true

// 修改尝试会抛出 UnmodifiableException 运行时异常 ALLOWED_METHODS.add( "TRACE" ) // 抛出异常!

// 解冻以获取可修改的副本进行扩展 extended = ALLOWED_METHODS.toModifiable() extended.add( "HEAD" ) extended.size() // 6——ALLOWED_METHODS 仍为5


这与 BoxLang 类中声明的常量完美配合:

class HttpConstants {

SAFE_METHODS    = setOf( "GET", "HEAD", "OPTIONS" ).toUnmodifiable()
UNSAFE_METHODS  = setOf( "POST", "PUT", "DELETE", "PATCH" ).toUnmodifiable()
ALL_METHODS     = ( SAFE_METHODS + UNSAFE_METHODS ).toUnmodifiable()

function isSafe( required string method ) {
    return SAFE_METHODS.contains( method.uCase() )
}

}


## JSON 序列化

集合序列化为 JSON 数组,因此可以与任何使用 JSON 的 API 进行无缝往返:

s = setNew( type="linked", values=[ "boxlang", "jvm", "oss" ] )

json = jsonSerialize( s ) // ["boxlang","jvm","oss"]

// 或者通过成员函数 json = s.toJSON()

// 嵌套在结构体中 payload = { "user" : "alice", "roles" : setOf( "editor", "viewer" ), "tags" : setNew( type="linked", values=[ "premium", "beta" ] ) } jsonSerialize( payload ) // {"user":"alice","roles":["editor","viewer"],"tags":["premium","beta"]}


## 快速 BIF 参考

| BIF | 成员函数 | 用途 |
|-----------------------------------------------|--------------------------------------|------------------------|
| `setNew( [type], [values], [caseSensitive] )` | -- | 创建新集合 |
| `setOf( ...values )` | -- | 通过可变参数创建 |
| `boxSetAdd( set, value )` | `s.add( v )` | 添加一个元素 |
| `boxSetAddAll( set, collection )` | `s.addAll( col )` | 添加多个元素 |
| `boxSetRemove( set, value )` | `s.remove( v )` | 移除一个元素 |
| `boxSetRemoveAll( set, collection )` | `s.removeAll( col )` | 移除多个元素 |
| `boxSetRetainAll( set, collection )` | `s.retainAll( col )` | 仅保留指定元素 |
| `boxSetClear( set )` | `s.clear()` | 清除所有元素 |
| `boxSetContains( set, value )` | `s.contains( v )` | 成员检测 |
| `boxSetContainsAll( set, col )` | `s.containsAll( col )` | 全部成员检测 |
| `boxSetIsEmpty( set )` | `s.isEmpty()` | 空集合检测 |
| `boxSetEquals( a, b )` | `a.equals( b )` | 相等性 |
| `boxSetIsSubsetOf( a, b )` | `a.isSubsetOf( b )` | 子集检测 |
| `boxSetIsSupersetOf( a, b )` | `a.isSupersetOf( b )` | 超集检测 |
| `boxSetIsDisjointFrom( a, b )` | `a.isDisjointFrom( b )` | 无交集检测 |
| `boxSetUnion( a, b )` | `a.union( b ) / a + b` | 所有元素 |
| `boxSetIntersection( a, b )` | `a.intersection( b ) / a * b` | 共有元素 |
| `boxSetDifference( a, b )` | `a.difference( b ) / a - b` | A 减 B |
| `boxSetSymmetricDifference( a, b )` | `a.symmetricDifference( b ) / a ^ b` | 二者其一,非两者 |
| `boxSetEach( set, cb )` | `s.each( cb )` | 迭代 |
| `boxSetMap( set, cb )` | `s.map( cb )` | 变换 |
| `boxSetFilter( set, cb )` | `s.filter( cb )` | 保留匹配项 |
| `boxSetReject( set, cb )` | `s.reject( cb )` | 移除匹配项 |
| `boxSetReduce( set, cb, init )` | `s.reduce( cb, init )` | 聚合成值 |
| `boxSetEvery( set, cb )` | `s.every( cb )` | 全部匹配? |
| `boxSetSome( set, cb )` | `s.some( cb )` | 任意匹配? |
| `boxSetNone( set, cb )` | `s.none( cb )` | 无匹配? |
| `boxSetFind( set, cb )` | `s.find( cb )` | 查找首个匹配 |
| `boxSetSize( set )` | `s.size() / s.len()` | 元素数量 |
| `boxSetToArray( set )` | `s.toArray()` | 转换为数组 |
| `boxSetToList( set, [delim] )` | `s.toList( [delim] )` | 转换为列表字符串 |

## 总结

`BoxSet` 并非锦上添花的功能。它是语言中缺失的一种基础集合类型,现在出现在你需要的任何地方:字面语法、运算符、函数式管道、JSON、Java 互操作以及类型系统本身。

仅运算符重载(`+`、`-`、`*`、`^`)就能将多步集合代数转化为单个表达式。三种底层变体确保你总能获得适合用例的性能特点。完整的函数式管道让你的代码保持声明式和可组合性,而非命令式和脆弱。

`BoxSet` 随 **BoxLang 1.14.0** 一起发布。立即更新,尝试字面语法,下次当你发现自己用循环对数组去重时,请使用集合。

**资源**:

- [BoxSet 文档](https://boxlang.ortusbooks.com/boxlang-language/syntax/sets "BoxSet Documentation")
- [Set BIF 参考](https://boxlang.ortusbooks.com/boxlang-language/reference/built-in-functions/set "Set BIF Reference")
- [BoxLang 下载](https://boxlang.io/?_gl=1*f0apz8*_gcl_au*MzI0MjI3ODM0LjE3NzU1MDUwMDA.*_ga*MTQ4MjQzODA2Ny4xNzc1NTA1MDAw*_ga_D1P6P1YYT0*czE3ODE2MTg5OTYkbzU3JGcxJHQxNzgxNjE5MDcwJGo2MCRsMCRoMA..*_ga_663JFQ7YGX*czE3ODE2ODk4NjMkbzUzJGcwJHQxNzgxNjg5ODYzJGo2MCRsMCRoMA.. "BoxLang Downloads")
- [ForgeBox](https://forgebox.io/ "ForgeBox")
- [社区 Slack](https://boxteam.ortussolutions.com/ "Community Slack")

*BoxLang 由 [Ortus Solutions](https://www.ortussolutions.com/ "Ortus Solutions") 构建——他们是 ColdBox、CommandBox 以及超过 350 个开源库背后的团队。BoxLang 是一种现代化的动态 JVM 语言,旨在随处运行:Web、Lambda、CLI、桌面、Android 等。*

本文首发于 [foojay](https://foojay.io):[BoxLang 1.14.0 : BoxSet 来了,BoxLang 全新的第一类集合类型](https://foojay.io/today/boxlang-1-14-0-boxset-is-here-boxlangs-new-first-class-set-type/)