在Ruby中玩转方法:理解方法调用与参数传递
关于ruby中方法的一些知识拓展
- 一个方法的返回值通常是方法中最后一个表达式的值,但是如果最后一个表达式的值不存在,则方法返回的值为nil。
- 如果一个方法最后一个表达式产生多个值,返回的值需要用逗号分隔开,最后方法会以数组的形式返回多个值。为了是代码更加清晰,可以将返回的值显示的放入数组中然后返回。见下面示例:
def name
puts "Hello World!"
end
p name #=> nil
def value(a, b)
return a, b
end #=> [a, b] 两种形式结果一样,需要注意第一种形式,必须跟上return关键字,否则会报错。
def value(a, b)
[a ,b]
end
当有多个返回值时,可以使用并行赋值的方式。
def double(a, b)
return a, b
end
first, second = double(1, 2)
p first, second
单实例方法
单实例方法从名字可以推断出,方法是针对单个实例来定义的,对于其他实例并不可见。
str = "Hello"
def str.world
puts "#{self} world"
end
str.world #=> hello world 可以调用world方法
str2 = "hello2"
str2.world #=> error,程序会报错,因为world方法仅针对hello字符串实例,而hello2字符串实例不可见
单实例方法定义并不是适用于所有的实例对象:
num = 10
def num.hello
p self
end
num.hello #=> error can not singleton
sym = :matz
def sym.hello
p self
end
sym.hello # error can not singleton
在ruby中,通常将Fixnum和Symbol类以及Numeric的实例视为intermediate值,因此并不是真实的对象引用,所以不能对它们定义单实例方法。
由于在ruby中万物皆对象,因此ruby中的不同类各自所定义的方法相对于Class来说,都是单实例方法,因为ruby中不同的类本质上来说也是Class类的对象。ruby中还有Module类,我们定义的module对于Moudle类来说也是它的实例,因此不同的module自己定义的方法相对于Module来说也是单实例方法。
module Foo
...
end
p Foo.class #=> Module, Foo是Moudle类的一个实例
Undefining method
在ruby中,定义的方法可以通过undef语句来取消定义
def foo; puts foo; end # 定义一个方法
undef foo #=> 取消定义的方法
foo #=> error, 方法不存在
对于类来说,类定义的方法也能被undef,以及类从父类继承而来的方法也能被undef,但是undef并不会影响父类的方法。本质上来说就是在继承某个类的方法时,屏蔽某些不需要的方法,因此并不会对父类的方法造成影响。
class A
def m
...
end
end
Class B < A
undef m
end
B.new.foo #=> "foo", 打印结果,可以正常调用从A类继承而来的方法foo
class A
def foo
p "foo"
end
end
class B < A
undef_method :foo # undef从A类继承而来的方法m,这样B的实例无法调用该方法
end
B.new.foo #=> 报错,提示为定义该方法
undef和undef_method的效果一样,undef_method的参数为函数名的符号名。通常将函数undefined并不常见,通常是将继承而来的方法重新定义或者进行扩展。
method alias
ruby中可以为method定义一个别名,使其它不同使用场景下面更符合人类的表达方式。
别名的作用更多的用于扩展一个方法的功能,通过别名可以在新的方法里面再次被调用
def hello
puts "hello"
end
alias old_hello hello #定义一个hello的别名 old_hello
def hello
puts "This is the new method"
puts "pay attention here!"
old_hello #这里再次调用先前定义的hello方法,因为old_hello方法是hello的别名
end
hello
需要注意的是,alias并不是overloading。
方法的括号
在ruby中,方法的括号在大部分情况情况下是可以省略的,这样看起来会更加的简洁。一般情况下,当一个方法调用不需要参数或者只需要一个参数的时候可以省略括号,但是在少数情况下,即使方法需要多个参数,括号也依然能够省略。
x = "hello"
x.length
x.length() #=>这两种方式在ruby中都被允许。
num = 1
1.between?1, 5
1.between?(1, 5) #=> 这两种方式在ruby中也是都被允许的
括号在方法调用时有两个作用:
- 在进行方法调用时,包围参数列表
- 对表达式进行分组
当使用括号进行方法调用时,方法后面必须立即跟上一个括号,括号与方法名之间没有空格,如果添加了空格,则ruby会把括号当做是表达式分组,而不是方法的参数列表。
def sum (a, b = 0)
a + b
end
puts sum 1, 2 #=> 先计算方法sum的值,然后将sum的返回值打印出来,结果为3, 这种写法在早期ruby1.8版本会产生警告,现在则不会。
puts sum(1, 2) #=> 同上,这种写法更加清晰,结果为3
puts sum(1, 2) * 2 #=> 先计算方法sum的值,然后将计算的值与2相乘,puts再将结果打印出来,结果为6
puts(sum 1, 2) #=> 这种写法puts会将sum的返回值当成自己的参数打印出来,结果为3,这种写法在早期ruby 1.8版本会产生一个warning,而现在的版本解释器会正常解析这种写法。
puts (sum 1, 2) #=> 这种写法在puts和括号之间加了个空格,ruby会把括号当成表达式分组,因此会先计算括号里面表达式的值,然后交由puts打印出来,结果为3
puts sum 1, 2 * 2 #=> 先计算2*2的值为4,然后将1和4传入方法sum,得到5,然后由puts打印出来
puts sum(1, 2) * 2 #=> 先计算sum的值为3,然后将3与2相乘得到6,交由puts打印出来
puts sum (2 + 2) * 3 #=> 先计算2+2的值为4,然后与3相乘得到12,再交由sum处理,由于sum设置了默认参数,因此即使传入一个参数也不会报错,此时会得到12,然后交由puts打印出来
puts 1, sum 1, 2 #=> 这种写法会使解释器无法正常执行计算,解释器不知道按照何种顺序去解析表达式。
puts 1, sum(1, 2) #=> 加上括号以后就可以让解释器明白sum后面的1和2是作为sum的参数列表的,然后1和sum的结果才是自己的参数列表。
puts 1, sum (1 + 2) #=> 这种写法也是不允许的,多了一个空格会造成歧义,使解释器无法正常解析表达式。
puts 1, (sum 2, 2) #=> 告诉ruby先计算sum函数的值,然后再将结果打印出来
通过以上的示例可以知道在ruby中,如果括号使用不正确会导致意想不到的结果,导致最终的结果与自己预期的结果不一致,因此最稳妥的办法还是老老实实在适当的位置加上该有的括号,避免歧义。
当然,ruby是一门和perl一样表达性非常强的语言,如果是经验丰富的rubist,那完全可以驾驭各种写法,但是对于新人还是老老实实按照正常的语法来写,可以使自己的代码更加的清晰明了。
针对上面的示例,稳妥的写法是下面这样:
puts(sum(1, 2)) #=> 3
puts(1, sum(1, 2)) #=> 1
3
method parameters
ruby中的方法参数有以下几种定义方式:
- 默认参数, 默认参数在定义的时候,不允许将一个传统参数定义在两个默认参数的中间。当有多个默认参数时,给方法指定参数的时候,参数是从左往右开始分配的。
- 带*前缀的参数表示参数的数量可以是任意个,当传入多个参数时,会将这些参数转换为一个数组。带*的参数不能定义在两个默认参数之间,也不能定义在默认参数的前面,但是可以定义在传统参数的前面
- 当一个函数的返回值为数组,且这个函数的返回值作为另一个函数的参数,则可以在这个函数的前面加一个*号,来展开数组,以便作为另一个函数的参数。
示例
def first(a, b)
return a + b
end
def second(a, b)
return a, b
end
p first(*second(1, 2))
如果second前面不加*号,则会提示参数错误,加了*可以正常执行。
在ruby里面,enumerator也是可以splatter的对象,因此我们也可以这么做
def max(first, *rest)
return first > rest.max ? first : rest.max
end
p max(*"hello world".each_char)
"hello world".each_char返回的是一个enumerator对象,在它前面加上\*然后作为参数传给max方法,则它就被展开为一个数组交由max方法处理。
在早期的版本中,带*号的参数通常必须放在传统参数和默认参数的后面,但是现在的版本,它们之间的顺序可以变换,但是某些情况下还是会报错
def foo2(*a, b = 10, c)
p [a, b, c]
end
foo2(1, 2, 3,4,5, 6 ,7) # 会报错
def foo3(b = 10, a, *c)
p [a, b, c]
end
foo3(1, 2, 3, 4, 5) # 会报错
def foo4(a, *b, c)
p [a, b, c]
end
foo4(1, 2, 3, 4)
def foo5(a = 10, *b, c)
p [a, b, c]
end
foo5(1, 2, 3, 4)
用hash命名参数
def sequences(args)
a = args[:a] || 0
b = args[:b] || 1
c = args[:c] || 0
ar = []
a.times { |i| ar << b * i + c}
return ar
end
p sequences a: 10, b: 20, c: 30
p sequences({:a => 10, :b => 20, :c => 30})
p sequences(:a => 10, :b => 20, :c => 30)
ruby对于以上三种写法都支持,对于不加花括号的hash,称之为bare hash
block参数
def sequences2(n, m ,c)
ar = Array.new
i = 0
while i < n
ar << yield(m, i, c)
i += 1
end
return ar
end
value = sequences2(10, 20, 30) do |a, b, c|
a * b + c
end
p value
def sequences3(n, m, c, &block)
ar = []
i = 0
while i < n
ar << block.call(n, m ,c)
n += 1
end
return ar
end
value2 = sequences3(10, 20, 30) do |a, b, c|
a * b + c
end
p value2
使用&来调用方法
a, b = [1, 2, 3], [1, 2, 3]
sum = a.inject { |total, var| total + var }
p b.inject(sum) { |total, var| total + var }
c, d = [1, 2, 3], [1, 2, 3]
sum = Proc.new { |total, vars| total + vars }
vs = c.inject(&sum)
p d.inject(vs, &sum)
调用方法的时候使用&,需要注意这个参数必须放在最后的位置,并且这个&通常放在一个proc object前面。
在Ruby 1.9中,Symbol类定义了to_proc方法,允许将符号以&前缀并传递给迭代器。当这样传递符号时,假定它是方法的名称。 to_proc方法返回的proc对象调用其第一个参数的命名方法,并将其余的参数传递给该命名方法。规范的情况是这样的:给定一个字符串数组,创建一个这些字符串的新数组,转换为大写。 Symbol.to_proc使我们可以优雅地完成此操作,如下所示:
在方法调用的时候使用&放在Proc对象的前面,其实就等同于普通的带块方法的调用形式。在使用&的形式作为参数调用方法时,方法本身在定义的时候必须用&block的形式定义一个块参数,否则代码会报错。
示例一
words = %w{hello world matz}
p words.map(&:upcase) #=> words.map { |str| str.upcase }
objs = Proc.new { |a| a + 2 }
def first(a)
yield(a)
end
p first(1, &objs) # 如果方法内定义了yield,则方法也能正常运行
示例二:我们可以自己模拟一个map函数
def my_map(a, &block)
ary = []
p block.class
a.each do |var|
ary << block.call(var)
end
return ary
end
ar = ["hello", "world", "matz"]
p my_map(ar, &:upcase)
Procs and lambdas
当我们以一个块参数定义方法的时候,是可以直接让方法返回一个proc对象的
def makeproc(&p)
p # return Proc object
end
proc_object = makeproc { |x| x * x }
以这种方式创建的Proc object都是proc而不是lambda,所有Proc 对象都有call方法
创建Proc对象的三种方法
- Proc.new
def invoke(&p)
p.call
end
def invoke
Proc.new.call
end
- Kernel里面有一个lambda方法,用来创建Proc对象,但是这个Proc对象是lambda,而不是proc。
在ruby 1.8里面有一个kernel.proc方法来创建lambda,但是在1.9版本被修复了,因此kernel.proc会返回一个proc而不再是lambda。 - ->(x){ x * x }
第三种方法是lambda的另一种创建方式,小括号里面的是参数,而花括号里面的是代码块。在声明块变量的时候可以声明本地块变量,以防止被作用域内的同名变量覆盖。
在声明时,可以用分号来分隔,分号前面的为块变量,分号后面的为本地块变量。块变量可以使用默认参数的形式。
->(x) { x + 8 }
->(x, y = 10) { x + y }
->(x, y; a, b, c) { ... } #声明本地块变量,这种情况下括号不能省略
->x, y, z { x + y + z } # 如果没有声明本地块变量,那么括号可以省略
->{ ... } #如果没有参数,则可以省略括号
def compose(f, g)
->(a){ f.call(g.call(a)) }
end
back= compose(->(a){ a + 1 }, ->(a){ a * a })
def test_lambda
yield
end
test_lambda &->{puts "hello"}
test_lambda { puts "hello" }
test_lambda &Proc.new{ puts "hello"}
def tsl(a, &b)
p b.call(a)
end
objs = Proc.new { |a| a + 2 }
tsl(1, &objs)
lambda和proc的调用形式
def foo(a, &block)
block.call(a)
end
Proc里面定义了call方法来调用块对象,块对象不能像正常方法调用那样被调用。除了以call来调用外,ruby还加了另外几种方式来调用
block[a]
block.(a)
.()这种形式的操作符无法被重新定义,它只是call方法的语法糖,但凡是定义了call方法的对象都能以这种形式来调用,不局限于Proc对象
curry化
ruby在1.9版本加入了curry这个方法,这个方法可以返回被柯里化的lambda或者proc。当一个被柯里化的lambda或者proc以参数不足的形式调用的时候,它会返回一个应用了给定参数版本的proc或者lambda。它常用应用于函数式编程范例。
one = lambda { |x, y| x + y }
two = one.curry[3]
p two[20] + two[30]
three = Proc.new { |x, y, z| x + y + z }
p three.curry[1][2][4]
p three.curry[2, 3][6, 7, 8]
p three.curry[2, 3][3]
p three.curry[2, 3, 3]
proc参数个数
four = Proc.new {|x, y| x + y }
p four.arity # 2
four1 = Proc.new {|x, *y| x + y }
p ~four1.arity # 1
four2 = Proc.new {|*x| x }
p ~four2.arity # 0
four3 = Proc.new { |x = 10, *y| x + y }
p ~four3.arity # 0
four4 = Proc.new { }
p four4.arity # 0
proc对象拥有一个arity的方法,这个方法可以返回这个proc对象需要多少参数,如果proc的参数前缀带*号,则该方法会返回一个负值,通过~操作符可以将这个结果转换来获取实际需要的参数值。
具体使用见上面例子。
Proc对象相等性
在比较两个Proc是否相等的时候,并不是它们的代码块相同,参数相同就表示这两个Proc对象是相等的。另外Proc对象有一个==方法来判断两者是否相等。
first_1 = Proc.new { |x| x }
first_2 = Proc.new { |x| x }
p first_1==first_2 # false
second_1 = Proc.new { |x| x }
second_2 = second_1.dup
p second_1==second_2 # false
p second_1.__id__ == second_2.__id__ # false
proc与lambda之间的区别
proc是块的对象形式,而lambd的行为则更像是方法。调用一个proc对象更像是yielding一个块,而调用lambda更像是调用方法。在ruby1.9多了一个实例方法来判断一个proc对象到底是proc还是lambda。
这个方式是lambda?
p Proc.new {}.lambda? # false
p lambda {}.lambda? # true
proc和lambda的区别还体现在控制语句
- return语句
def test
proc = Proc.new { puts "hello world"; return }
proc.call
puts "hello god" # no execute
end
test
在proc对象里面使用return语句,它会直接从方法中返回,而不是仅仅只从块里面返回,上面的例子中,一旦proc对象返回,则整个方法也被返回,因此最后一句代码不会被执行。
再看下面一个例子:
def procBulider
proc = Proc.new { puts "hello world"; return }
return proc
end
test
def test2
p = test
p.call
puts "hello world"
end
test2
当我们从一个proc构造方法返回一个proc对象,然后在test2方法中调用时会产生一个错误localjumpError, 因为return在块里面不但会从块返回,还会从方法中返回,因此一旦在某个调用proc对象的位置返回,而这个位置是在另外一个方法中,则会报错。
修复这个问题方法很简单,就是把return移除,如果非要使用return语句,则可以使用另一个方式,用lambda来代替,因为lambda的行为更像方法,因此在它里面调用return只会从块里面返回,而不会整个方法中返回。
def lambdaBulider
proc = ->{ puts "hello world"; return }
return proc
end
def test3
p = lambdaBulider
p.call
puts "hello world"
end
test3 # 能够正常执行
- break语句
def test5
p = Proc.new { break }
p.call
puts 'test'
end
test5 # localjumpError
在proc里面使用break依然会报错,因为break也会提前从方法中返回,因此会产生异常。
def iterator(&block)
puts "enter iterator"
block.call
puts "exiting iterator"
end
def test6
iterator { break }
end
test6 # 正常运行,但是不会打印方法最后一个表达式,因为break让方法提前退出了
在lambda里面使用break没有任何意义,它的作用就像return一样,让方法返回
def test7
p = lambda { break }
p.call
puts "hello world"
end
test7 # 程序正常运行,它仅仅只是终止了lamdba,而不会终止调用这个lambda的方法
- next语句
def test8
p = Proc.new { next 10 }
p p.call
puts "hello world!!"
end
test8
next语句在block,proc或者lambda的作用一样,它会从一个块返回,但是不会从方法返回,并且如果next后面跟上一个表达式的话,它会返回表达式的值。
- retr
下一篇: 评估对话系统的标准与度量方法
推荐阅读
-
一种结构设计模式,允许在对象中动态添加新行为。它通过创建一个封装器来实现这一目的,即把对象放入一个装饰器类中,然后把这个装饰器类放入另一个装饰器类中,以此类推,形成一个封装器链。这样,我们就可以在不改变原始对象的情况下动态添加新行为或修改原始行为。 在 Java 中,实现装饰器设计模式的步骤如下: 定义一个接口或抽象类作为被装饰对象的基类。 公共接口 Component { void operation; } } 在本例中,我们定义了一个名为 Component 的接口,该接口包含一个名为 operation 的抽象方法,该方法定义了被装饰对象的基本行为。 定义一个实现基类方法的具体装饰对象。 公共类 ConcreteComponent 实现 Component { public class ConcreteComponent implements Component { @Override public void operation { System.out.println("ConcreteComponent is doing something...") ; } } 定义一个抽象装饰器类,该类继承于基类,并将装饰对象作为一个属性。 公共抽象类装饰器实现组件 { protected Component 组件 public Decorator(Component component) { this.component = component; } } @Override public void operation { component.operation; } } } 在这个示例中,我们定义了一个名为 Decorator 的抽象类,它继承了 Component 接口,并将被装饰对象作为一个属性。在操作方法中,我们调用了被装饰对象上的同名方法。 定义一个具体的装饰器类,继承自抽象装饰器类并实现增强逻辑。 公共类 ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component 组件) { super(component); } } public void operation { super.operation System.out.println("ConcreteDecoratorA 正在添加新行为......") ; } } 在本例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,它继承自装饰器抽象类,并实现了操作方法的增强逻辑。在操作方法中,我们首先调用被装饰对象上的同名方法,然后添加新行为。 使用装饰器增强被装饰对象。 公共类 Main { public static void main(String args) { Component 组件 = new ConcreteComponent; component = new ConcreteDecoratorA(component); 组件操作 } } 在这个示例中,我们首先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传递。最后,调用装饰器的操作方法,实现对被装饰对象的增强。 使用场景 在 Java 中,装饰器模式被广泛使用,尤其是在 I/O 中。Java 中的 I/O 库使用装饰器模式实现了不同数据流之间的转换和增强。 让我们打开文件 a.txt,从中读取数据。InputStream 是一个抽象类,FileInputStream 是专门用于读取文件流的子类。BufferedInputStream 是一个支持缓存的数据读取类,可以提高数据读取的效率,具体代码如下: @Test public void testIO throws Exception { InputStream inputStream = new FileInputStream("C:/bbb/a.txt"); // 实现包装 inputStream = new BufferedInputStream(inputStream); byte bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); } } } } 其中 BufferedInputStream 对读取数据进行了增强。 这样看来,装饰器设计模式和代理模式似乎有点相似,接下来让我们讨论一下它们之间的区别。 第三,与代理模式的区别: 代理模式的目的是控制对对象的访问,它在对象外部提供一个代理对象来控制对原对象的访问。代理对象和原始对象通常实现相同的接口或继承相同的类,以确保两者可以相互替换。 装饰器模式的目的是动态增强对象的功能,而这是通过对象内部的包装器来实现的。在装饰器模式中,装饰器类和被装饰对象通常实现相同的接口或继承自相同的类,以确保两者可以相互替代。装饰器模式也被称为封装器模式。 在代理模式中,代理类附加了与原类无关的功能。
-
在Ruby中玩转方法:理解方法调用与参数传递
-
玩转Java底层:JMX详解 - jconsole与自定义MBean监控工具的实际应用与区别" 在日常JVM调优中,我们熟知的jconsole工具通过JMX包装的bean以图形化形式展示管理数据,而像jstat和jmap这类内建监控工具则由JVM直接支持。本文将以jconsole为例,深入讲解其实质——基于JMX的MBean功能,包括可视化界面上的bean属性查看和操作调用。 MBeans在jconsole中的体现是那些可观察的组件属性和方法,如上图所示,通过名为"Verbose"的属性能看到其值为false,同时还能直接操作该bean的方法,例如"closeJerryMBean"。 尽管jconsole给我们提供了直观的可视化界面,但请注意,这里的MBean并非固定不变,开发者可根据JMX提供的接口将自己的自定义bean展示到jconsole。以下步骤展示了如何创建并注册一个名为"StudyJavaMBean"的自定义MBean: 1. 首先定义接口`StudyJavaMBean`,接口需遵循MBean规范,即后缀为"MBean"且包含getter方法代表属性,如`getApplicationName`,和无返回值的setter方法代表操作,如`closeJerryMBean`。 ```java public interface StudyJavaMBean { String getApplicationName(); void closeJerryMBean(); } ``` 2. 编写接口的实现类`StudyJavaMBeanImpl`,实现接口中的方法: ```java public class StudyJavaMBeanImpl implements StudyJavaMBean { @Override public String getApplicationName() { return "每天学Java"; } @Override public void closeJerryMBean() { System.out.println("关闭Jerry应用"); } } ``` 3. 在代码中注册自定义MBean,涉及的关键步骤包括: - 获取平台MBeanServer - 定义ObjectName,指定唯一的MBean标识符 - 注册MBean到服务器 - 启动RMI连接器服务,以便jconsole能够访问 ```java public void registerMBean() throws Exception { // ... 具体实现省略 ... } ``` 实际运行注册后的MBean,您将在jconsole中发现并查看自定义bean的属性和调用相关方法。然而,这种方式相较于传统的属性/日志查看和HTTP接口,实用性相对有限,可能存在潜在的安全风险。但不可否认的是,JMX及其MBean机制对于获取操作系统信息、内存状态等关键性能指标仍然具有重要价值。例如: 1. **获取操作系统信息**:通过JMX MBean,可以直接获取到诸如CPU使用率、操作系统版本等系统级信息,这对于资源管理和优化工作具有显著帮助。
-
Grid++Report 锐浪报表开发常见问题解答集锦-报表设计 问:怎样在设计时打印预览报表? 答:为了及时查看报表的设计效果,Grid++Report 报表设计应用程序提供了四种查看视图:普通视图、页面视图、预览视图与查询视图。通过窗口下边的 Tab 按钮可以在四种视图中任意切换。在预览视图中查看报表的打印预览效果,在查询视图中查看报表的查询显示效果。如果在报表的记录集提供了数据源连接串与查询 SQL,在进入预览视图与查询视图时会利用数据源连接串与查询 SQL 从数据源中自动取数,否则 Grid++Report 将自动生成模拟数据进行模拟打印预览与查询显示。注意:在预览视图与查询视图中看到的报表运行结果有可能与在你程序中的最终运行结果有差异,因为在报表的生成过程中我们可以在程序中对报表的生成行为进行一定的控制。 问:怎样用 Grid++Report 设计交叉表? 答:Grid++Report 没有提供专门实现交叉表的功能,其它的报表构件提供的交叉表功能一般也比较死板和功能有限。利用 Grid++Report 的编程接口可以做出灵活多变,功能丰富的交叉表。示例程序 CrossTab 就是一个实现交叉表的例子程序,认真领会此例子程序,你就可以做出自己想要各种交叉表,并能提取一些共用代码,便于重复使用。 问:怎样设置整个报表的缺省字体? 答:设置报表主对象的字体属性,也就是设置了整个报表的缺省字体。如果改变报表主对象的字体属性,则没有专门的设置字体属性的子对象的字体属性也跟随改变。同样每个报表节与明细网格也有字体属性,他们的字体属性也就是其拥有的子对象的缺省字体。 问:怎样在打印时限制一页的输出行数? 答:设定明细网格的内容行的‘每页行数(RowsPerPage)’属性即可。另外要注意‘调节行高(AdjustRowHeight)’属性值:为真时根据页面的输出高度自动调整行的高度,使整个页面的输出区域充满。为假时按设计时的高度输出行。 问:怎样显示中文大写金额? 答:将对象的“格式(Format)”属性设为 “$$” 及可,可以设置格式的对象有:字段(IGRField)、参数(IGRParameter)、系统变量(IGRSystemVarBox)与综合文字框(IGRMemoBox),其中综合文字框是在报表式上设格式。 问:能否实现自定义纸张与票据打印? 答:Grid++Report 完全支持自定义纸张的打印,只要在报表设定时在页面设置中选定自定义纸张,并指定准确的纸张尺寸。当然要在最终输出时得道合适的打印结果,输出打印机必须支持自定义纸张打印。Windows2000/XP/2003 操作系统上可以在打印机上定义自定义纸张,也可以采用这种方式实现自定义纸张打印。 问:怎样实现 0 值不打印? 答:直接设置格式串就可以,在“数字格式”设置对话框中选定“0 不显示”,就会得到合适的格式串。也可以通过直接录入格式串来指定 0 不显示,但格式串必须符合 Grid++Report 的规定格式。另一种实现办法是在报表获取明细记录数据时,在 BeforePostRecord 事件中将值为零的字段设为空,调用字段的 Clear 方法将字段置为空。 问:怎样实现多栏报表? 答:在明细网格上设‘页栏数(PageColumnCount)’属性值大于 1 即可。通过 Grid++Report 的“页栏输出顺序”还可以指定多栏报表的输出顺序是“先从上到下”还是“先从左到右”。 问:如何实现票据套打? 答:Grid++Report 为实现票据套打做了很多专门的安排:报表设计器提供了页面设计模式,按照设定的纸张尺寸显示设计面板,如果将空白票据的扫描图设为设计背景图,在定位报表内容的输出位置会非常方便。报表部件可以设定打印类别,非套打输出的内容在套打打印模式下就不会输出。 问:Grid++Report 有没有横向分页功能? 答:回答是肯定的,在列的总宽度超过打印页面的输出宽度时,Grid++Report 可以另起新页输出剩余的列,如果左边存在锁定列,锁定列可以在后面的新页中重复输出,这样可以保证关键数据列在每一页都有输出。仔细体会 Grid++Report 提供的多种打印适应策略,选用最合适的方式。Grid++Report 的多种打印适应策略为开发动态报表提供了很好的支持。 问:怎样实现报表本页小计功能? 答:定义一个报表分组,将本分组定义为页分组,在本分组的分组头与分组尾上定义统计。页分组就是在每页产生一个分组项,在每页的上端与下端都会分别显示页分组的分组头与分组尾,页分组不用定义分组依据字段。 报表运行 问:怎样与数据库建立连接? 答:如果在设计报表时指定了数据集的数据源连接串与查询 SQL 语句,Grid++Report 采用拉模式直接从数据源取得报表数据,Grid++Report 利用 OLE DB 从数据源取数,OLE DB 提供了广泛的数据源操作能力。如果 Grid++Report 的数据来源采用推模式,即 Grid++Report 不直接与数据库建立连接,各种编程语言/平台都提供了很好的数据库连接方式,并且易于操作,应用程序在报表主对象(IGridppReport)的 FetchRecord 事件中将数据传入,例子程序提供了各种编程语言填入数据的通用方法,对C++Builder 和 Delphi 还进行了专门的包装,直接关联 TDataSet 对象也可以将 TDataSet 对象中的数据传给报表。 问:打印时能否对打印纸张进行自适应?支持表格的折行打印吗? 答:Grid++Report 在打印时采用多种适应策略,通过设置明细网格(IGRDetailGrid)的‘打印策略(PrintAdaptMethod)’属性指定打印策略。(1)丢弃:按设计时列的宽度输出,超出范围的内容不显示。(2)绕行:按设计时列的宽度输出,如果在当前行不能完整输出,则另起新行进行输出。(3)缩放适应:对所有列的输出宽度进行按比例地缩放,使总宽度等于页面的输出宽度。(4)缩小适应:如果列的总宽度小于页面的输出宽度,对所有列的输出宽度进行按比例地缩小,使总宽度等于页面的输出宽度。(5)横向分页:超范围的列在新页中输出。(6)横向分页并重复锁定列。 问:如何改变缺省打印预览窗口的窗口标题? 答:改变报表主对象的‘标题(Title)’属性即可。 问:利用集合对象的编程接口取子对象的接口引用,但不是自己期望的结果。 答:Grid++Report中所有集合对象的下标索引都是从 1 开始,另按对象的名称查找对象的接口引用时,名称字符是不区分大小写的。 问:怎样在运行时控制报表中各个对象的可见性?即怎样在运行时显示或隐藏对象? 答:在报表主对象(GridppReport)的 SectionFormat 事件中设定相应报表子对象的可见(Visible)属性即可。 问:报表主对象重新载入数据,设计器中为什么没有反映新载入的数据? 答:应调用 IGRDesigner 的 Reload 方法。 问:怎样实现不进入打印预览界面,直接将报表打印出来?
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面