Ruby元编程笔记

1.常用方法:

  • Object#class 具体的类信息。[注]#表示实例方法,.表示类方法。
  • Object#class.instance_methods(false) 显示public的实例方法。false表示只显示自己的方法,而不是继承来的。
  • Object#instance_variables 实例变量列表。和Java不同,Ruby中同一个类的不同对象可能有不同的实例变量列表,实例变量存放在对象中。
  • Object#methods 返回所有的公共实例方法。String.instance_methods == "aaa".methods 类的实例表方法表示它的对象的可以访问的方法。
  • Object#singleton_methods 可以得到对象的单件方法或类方法。通过true/false来表示是否显示include的模块的方法。
  • Module.constants 表示当前环境所有的常量,包括定义的类名称。
  • Module#constants 表示当前实例的常量,例如A.constants。由于A是一个Class的变量,Class继承自Module,所以A也就有了constants实例方法。
  • Module#ancestors 可以获取类的父类列表(包括模块),模块会按照声明的顺序正好在声明类的上一层。可以根据这个了解方法调用的路径。 self标识调用方法的对象,在类定义中self表示类自己。
  • Object#send(:method,args…) 可以直接调用对象的方法,这个方法可以调用私有方法,如果为了隐私可以使用public_send
  • Module#define_method方法可以给类定义对象。

2.基本概念:

方法存放在类里,这里的可能是正常的类也可能是eigenclass,每个对象都有一个eigenclass,包括类自身。实例方法存放在类中,单件方法存放在eigenclass中,类方法是存放在类的eigenclass中。

obj是变量,Object、Class、String等是常量,它们也都是Class.new出来的变量。在Ruby中所有对象均是类,也都是对象。只要搞清楚当前执行环境的self、当前类就好理解很多了,self是方法的调用主体,当前类是方法定义的主体。

Object中包含methods等方法的定义,Module中包含instance_methods等方法的定义。由于类如String,MyClass都是Class的实例,所以都可以访问instance_methods,也由于这些类是Class的实例,所以他们可以使用Class的实例方法如new等来定义自己实例对象。上面也说明Object是为实例对象服务的,里面有很多实例方法。而Module、Class是为类服务的,里面有很多类定义相关的方法。

load('load.rb',true)中load.rb中的变量会在作用域之外,我们看不到。但是常量如果不加true会影响现有的作用域,如果仅仅是要执行的结果可以加true,如果还需要常量定义,可以不加或是使用require。

可以通过打开类冲定义方法、新增方法等:

#打开类实例
class String
  def to_alphanumberic
    gsub /[^\w\s]/, ''
  end
end

3.方法的查询路径:

  1. 查询自身(自身的单件类中是否有此方法)
  2. 查询自身所属的类的实例方法
  3. 查询自身所属类的父类的实例方法
  4. 找不到方法则执行method_missing方法

4.method_missing方法:

当某个方法沿着继承链找不到时会调用method_missing方法。核心库delegate动态代理就利用了这个特性,很多适配器库也使用了这个特性。

使用method_missing的问题:

  1. 方法可能导致死循环,使用时应注意。可以采用白名单和super来处理。
  2. 它比平常方法要慢1倍左右。
  3. 当一个幽灵方法和真实方法冲突时,比如继承自Object的方法,真实方法会胜出。这时可以使用白板类,这个类比Object类的方法还要少。ruby1.9之后可以直接继承Basic_Object来变成白板类。
class BlankSlate
  #在一个白板类中隐藏名为给定name的方法
  #但不隐藏instance_eval方法或任何一“__"打头的方法
  def self.hide(name)
    if instance_methods.include?(name.to_s) and
      name  !~ /^(__|instance_eval)/  #|instance_method 可能需要加上
      @hidden_methods ||={}
      @hidden_methods[name.to_sym] = instance_method(name)
      undef_method name
    end
    instance_methods.each { |m| hide(m)}
    #...
end

Module#const_missing()当某个常量找不到时会调用这个方法。如果是在具体类或是Object中定义,则这个类的实例或是所有对象都可以使用此方法。

5.块Block:

在代码中可以通过{}或是do...end关键字来传递块。在代码中可以使用yield(a,b)来调用块。方法中可以通过Kernel#block_given?方法判断当前是否传递了块。

def method2
  (1..10).each do |x|
    yield(x)
    end
end
method2 do |x|
  puts x if x != 4
  break if x == 4
end
#这里的break会对method2中的each生效,打印结果为1,2,3

method2 do |x|
  y ||= 1
  y= y + 1
  puts y
end
#会一直打印2,说明y每次都被初始化。

Ruby中有4中方法可以打包代码以备后用:

  1. 使用块(块不是对象);
  2. 使用Proc。(Proc是块转换的对象)
  3. 使用lambda。lambda会校验参数数目。
  4. 使用方法。

代码 + 绑定 = 块。可以通过binding方法获取当前绑定。eval方法可以指定binding。

块转换为Proc的方法:1.Proc.new; 2.lambda() 3.proc().4.&操作符。然后使用#call()方法执行。

def math(a,b)
  yield(a,b)
end
def teach_math(a,b,&operation)
  puts "Let’s do the math:"
  puts math(a,b, &operation)
end
teach_math(2,3) {|x,y| x+y}

操作符如+-的类型是Proc,可以直接使用call来执行,再加上&可以把Proc转换为块。

6.Ruby的作用域:

Ruby的作用域没有嵌套的概念,当作用域切换时只能看到新的作用域。有3个方式可以重新定义作用域:类定义、模块定义、方法定义。

扁平作用域:Kernel#instance_eval 可以把执行它的对象作为块的self对象,从而块可以访问对象中的所有实例变量、私有方法等。这种块也是扁平作用域,也称为”上下文探针”。#instance_exec功能类似,但是它可以给块传参数。

其实扁平作用域就是利用Class.newclass_evalinstance_eval等在不切换作用域的情况下完成类定义、方法定义、在对象内部执行代码等功能。

使用Module#class_eval在不知道类名的时候打开类,定义方法。它会把当前类切换为当前类,同时设置了self变量。Ruby总是会跟踪当前类,这个和self是不同的,它是self的类。

instance_eval也会修改当前类,它修改的是接收者的eigenclass.

#method1只对a有效。
a.instance_eval do
  def method1; ok;end
end

7.eigenclass:

元类是对象自身的类,可以通过下面的方法获取它。

class BasicObject
  def eigenclass
    class << self; self ; end
  end
end

对象元类的父类是它所属的类。obj.eigenclass.superclass = obj.class

类的元类的父类是类的父类的元类(为了支持类方法继承)。特殊的BasicObject的元类是Class。Object.eigenclass.superclass = BasicObject.eigenclass = Class

单件singleton方法:

str ="hello str"
def str.title?
  self.upcase == self
end

也可以通过下面这种方式定义:

class << an_object
  #这里就是eigenclass的作用域了
end

irb中定义的方法,是作为Object的私有方法存在的。因为irb的self对象是main,是Object的一个实例对象;irb中当前类是Object。

def method1
  puts "method1"
end
"aa".send :method1

8.其他法术

类宏:本质是类中的类方法。

#例子:声明取消的方法:
class Book
  def subtitle
    #...
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method} is deprecated.Use #{new_method}"
      send(new_method, *args, &block)
    end
  end
  deprecate :title2, :subtitle
end

include模块:在类中调用Module#include方法包含其他模块,模块中定义的方法会成为类的实例方法(通过建立ancestors关系)。如果在类的eigenclass中include模块,模块中定义的方法会成为这个类的类方法,这样其实等效于调用Object#extend方法。

类扩展混入:通过使用钩子方法(ruby中很多钩子方法,包括继承时,包含时调用的)。

Module M
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def my_method
      'a class method'
    end
  end
end

class C
  inlcude M
end

C.my_method #=>"a class method"

空指针保护: a ||= []

参数数组: 使用*号来将多个值转换为数组。

*a = 1, 2, 3 #=> a = [1, 2, 3]

具名参数method(username: "hello", age: 13) 上述值会作为最后一个参数的值,最后一个参数会是hash数组。当默认参数、边长参数、具名参数混用时需要自己进行参数解析。

符号到Proc:把一个符号转换为调用单个方法的代码块:

#这里会自动调用Symbol#to_proc方法
#下面是这个方法的源码
class Symbol
  def to_proc
    Proc.new {|x| x.send(self)}
  end
end
# &符号可以作用于任何对象,会调用它的.to_proc方法.
[1, 2, 3, 4].map(&:even?) #=>[false, true, false, true] 

环绕别名:通过alais关键字定义方法别名,然后把原来方法重定义。

class String
  alias :real_length :length
  
  def length
    real_length > 5 ? 'long' : 'short'
  end
end
"war and peace".length  #=> 'long'
"war and peace".real_length #=> 13

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

线程安全简介

线程和进程都可以被操作系统直接调度,所不同的是多个线程之间会共享内存空间,但是多个进程之间的内存空间是相互隔离的。随着单核CPU的处理速度达到上限,多核时代正式到来,如何更好地利用多核资源成为了一个必须考虑的话题。在Java中倾向与使用多线程模型,Java针对多线程做了很多优化,包括提供了成熟的线程池1、线程安全工具类以及对锁的优化等。Java Web服务器也都是多线程的。但多线程编程一直以来都很困难且容易出错,虽然有很好的工具支撑,但编写代码时也需要格外注意,稍不小心就会掉进并发的陷阱。另一方面有些语言如Ruby则倾向于多进程的方式,由操作系统来实现并发且保证程序正确性,借助Linux的copy on write属性来节省内存。两种方式哪种更好已经超出了本文的范畴,这也是仁者见仁智者见智。值得一提的是Ruby on Rails 4开始正式支持多线程,Rails 5 更是使用puma作为默认的开发服务器,这也从另一角度预示着多线程的到来。

哪些情况是线程安全的?

下面主要讨论下多线程环境下的线程安全问题,线程安全主要是由于并发导致的,1.操作系统可能在任何时刻挂起某个线程;2.多核情况下两个线程可能处于并向执行的情况。其中第一点可以看出在单核情况下也会有线程安全问题。下面介绍哪些情况是线程安全的:

  1. 单线程环境中,包括多进程单线程、基于事件的程序(服务器)中,不存在多个线程同时修改或访问同一个变量的情况,这种情况下肯定线程安全的。为什么把这个放在第一位呢,因为如果你可以选择一个多进程单线程的Web服务器或是基于事件的服务器你就从根本上解决了线程安全的问题(当然可能有其他问题,比如内存消耗大、状态信息如session不一致等);
  2. 局部变量:即方法中声明的局部变量,这些是线程安全的,它们会随着方法调用结束而被回收,且只会对本线程可见。
  3. ThreadLocal全局变量:这些变量是线程级的,线程安全的,java会保证每个线程只有一份,每个线程只能访问属于自己的ThreadLocal变量。
  4. 线程安全的类:java中很多类会明确声明是线程安全的,例如concurrent包下类。这些类在访问是自身会进行加锁、原子化等操作来保证线程安全。
  5. 线程安全的静态类方法:这些类是单例的,其内部会保证线程安全,spring中很多类都属于此类。
  6. 无状态的bean。如没有状态的控制器bean。spring中的所有pojo类都可以是bean,且是单例的,这种类如果包括实例变量,不管共有私有都不一定是线程安全的。
  7. 不可变对象。例如java中的string类,以及scala中很多不可变的类。不可变类有很多优点,对于并发,可重复执行很有用。 上面提到的都是多个线程可以同时访问的变量,比如全局变量、类的静态变量、单例或是bean中的类实例变量(包括共有、私有)。

线程安全的优化方法

如果程序中确实需要包括这些共享信息,如何保证线程安全呢?可以采用如下措施:

  1. 将公共信息保存在数据库中,或是其他公共的位置。这样除了保证线程安全还可以保证一致性,但会牺牲效率。
  2. 对变量进行ThreadLocal包装,作为线程局部变量,做到线程间独立。
  3. 使用线程安全的类保存和处理这些共享信息(变量),如使用java的concurrent包的常用工具类
  4. 对共享信息的存取方法进行封装,并进行同步处理(加锁)。这里要注意不能只对set方法加锁,一方面是因为get方法不加锁不能保证取得的数据是最新的数据(参考java的happen before2),另一方面是除非你认真考虑了源码的各个分支情况,包括其底层调用的各个细节,否则读取方法也可能很危险,因为读取方法并没有规定不能进行其他处理,只是很多情况下它只是简单返回而已。SimpleDateFormat这个类在java中就不是线程安全的,我们有次想当然的把它作为静态变量,结果在日期格式化的时候出了问题,返回日期出现了错乱。
    [注]:Ruby中没有happen before的概念,取而代之的是GIL解释器锁3,这个锁会保证所有的单个native c方法的原子性。这样就导致在同一个进程内同时只有1个线程在使用CPU,如果是单进程单线程模式下甚至都不能充分使用1个CPU。如果要真正使用多核只能采用多进程。GIL的存在并不是必须的,只是对多线程实现的一个妥协。
  5. 如果多个状态不是独立的,这时即使每个状态是线程安全的,但整个对象也不是线程安全的,仍然需要同步处理(这样来看的话其实内部状态是不是线程安全就没什么意义了)。这时也可以使用下面要提的不变对象来处理。
  6. 对变量进行不变处理,使用不变对象保存状态。当更新状态时新建一个对象。在赋值时需要注意进行同步处理或是使用java的volatile标识。不变对象特别适合与同时保存多个关联的状态的情况,这样可以把多个状态当做1个来处理。
  7. 除非一个第三方类声明为线程安全,或是你认真读过它的代码,否则都可能有线程问题。这时需要进行同步处理或是进行充分测试。

如何保证整个程序的线程安全?4

  1. 框架自身是线程安全的
  2. 我们自己的应用代码是线程安全的
  3. 我们依赖的第三方代码是线程安全的

线程安全编程是很困难的,也是bug事故的多发区,除非性能要求否则尽量不要贸然使用多线程。现在很多web服务本身就是多线程的,进而我们的应用本身就是多线程的,在开发时也要小心,尽量不使用有状态的类。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Rails datetime类型在mysql数据库中的精度问题

在我的博客网站开发过程中,我在开发环境使用sqlite3,而在生产环境则使用性能更好的mysql。一直以来我也觉得没什么,因为rails会屏蔽所有的数据库差异。但这样也带来一些问题,其中一个就是schema.rb的版本控制问题。Rails建议对此文件进行版本控制,这样可以更清楚的看到数据库版本的变动。但由于开发和生产环境使用的数据库不一致,导致每次rake db:migrate后schema.rb都会变化,开发环境和生产环境的这个文件版本不一致(由于数据库的不同),将测试环境的schema.rb传到版本控制显然不好,但是不传的话总是提示有文件未提交感觉很不好。

经过权衡决定在测试环境也使用mysql,并且将测试数据也导入到新的mysql数据库。数据库不一致的另一个问题是我在迁移时发现的,不同的数据库还是有差异的,而且还影响到测试案例的成功与否,其中一个问题就是这次要重点介绍的datetime类型的精度问题。

datetime类型是rails在generate model时自动生成的,如果执行

$ bin/rails generate model Product name:string description:text 

会生成一个迁移文件如下:

#file name: db/migrate/20160501090706_create_products.rb
class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps null: false
    end
  end
end 

其中 t.timestamps null: false会生成2个字段created_at,updated_at,其schema.rb中的

#file name : schema.rb
#...
    t.datetime "created_at"
    t.datetime "updated_at"
#...

这种datetime类型的字段是作为时间戳使用的,在sqlite3中为6位精度,如2016-05-01 11:28:38.860909,这没有问题。但是换为使用mysql就有问题了,datetime 类型在mysql中默认精度只到秒,这显然不能满足要求,秒级的时间戳是没有意义的。在网上找了很多资料,有很多比较陈旧,下面把我测试通过的解决方法记录一下,方便大家参考1 2

在Rails 4.2的Release Notes有下面一句话:

Added support for fractional seconds for MySQL 5.6 and above

可以看出Rails 4.2已经支持日期精度功能,关于mysql的fractional seconds功能可以参考 mysql reference.

我们在Rails 4.2+应用中应该如何使用此功能呢?可以通过添加migrate文件来解决,具体如下:

 bin/rails g migration ChangeDatetimeLimitForMysql 

在生成的文件中修改如下:(下面只以修改一个表为例,多个表修改方法类似)

# file name: db/migrate/20160501090706_change_datetime_limit_for_mysql.rb
# mysql 5.6.4以上的版本支持分数精度(fractional seconds),默认mysql精度只到秒
# limit 置为 6 精确到微妙;如将limit 修改为 3 则精确到毫秒

class ChangeDatetimeLimitForMysql < ActiveRecord::Migration
  def up
    change_column :comments, :created_at, :datetime, limit: 6
    change_column :comments, :updated_at, :datetime, limit: 6

     #...
  end
  
  def down
    change_column :comments, :created_at, :datetime
    change_column :comments, :updated_at, :datetime

    #...
  end
end 

然后执行rake db:migrate即可,这样之后新增修改的记录的时间戳都会精确到微秒。注意测试环境也需要执行rake db:test:prepare使其生效。

[注]:此方案适用于Rails 4.2+,mysql 5.6.4+.

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

文件字符编码概述

作为一个程序员,不可避免要经常与字符编码打交道。代码乱码、数据乱码、报文乱码也是时常遇到。近期生产上也出现了乱码、非法字符问题,趁着这个机会对字符编码进行下总结。

1.文件内容格式概述

首先大家应该理解:文件其实是一系列的二进制流,界面展示(或程序执行)相当于对这些二进制流的一种转义。理解这一点再考虑后面的问题就简单多了。

对于文件来说里面可以存放任何内容,例如文件可以存放文本、图片、音乐、视频甚至是程序二进制可执行文件。不同的文件需要不同的软件才能进行查看,例如一般的文本可以用记事本、UltraEdit、vim等来查看;图片可以用系统自带的看图工具打开,但编辑则需要Photoshop等专业工具;音乐和视频文件也类似。为了进一步标识哪些文件可以用什么软件打开或编辑,就有了文件拓展名(后缀),例如.txt、.pdf、.mp3等等,文件拓展名只是一个标识,并不影响具体的文件内容和展示。

下面再说下二进制与文本文件的区别:文本文件是指你能够直接用文本编辑器打开,能直接读得懂。二进制文件则需要特定的软件来读取。

2.文本文件编码问题

文本文件是我们最常遇到的文件格式,它们的后缀有很多种比如txt,log,rb,java,h,cpp等等,当然也不局限于这些后缀。刚也说过了后缀并不影响文件的内容和展示,一个没有后缀的文本文件也可以正常打开和查看。文本文件其实也是一系列的二进制流,但它是遵循某个编码标准的(另外还有BOM等特殊规定),常见的编码标准有ASCII、GBK、UTF-8等等。这些编码定义了二级制和字符之间的映射关系,例如汉字“中”的UTF-8编码为E4 B8 AD,它的GBK编码为D6 D0。编码可以参考这些博文1 2

假设文件是用GBK编码写的,但是当成UTF-8格式来读,得到的结果肯定是不对的,可能会有乱码或者天书文字。当然了,现在编辑器还是挺智能的,它们一般能识别出文件使用的编码,所以看上去没什么问题。遇到乱码问题需要做两件事:1.确定文件使用的正确编码格式;2.利用iconv等编码转换工具将文件转换为其他你指定的编码;或者指定让编辑器采用和文件一致的格式来读取。

大家可能还遇到过一种事情,我的编码选对了,大部分字符都能正确显示,但仍然有部分是?、[]或是其他乱码,这种一种可能是文件本身有问题,另一种是字符集的问题。比如Unicode定义了U+00000-U+10FFFF所有的字符集(其中U+D800-U+DFFF为UTF-16的代理对字段),但是一般的字体、软件仅支持U+0000-U+FFFF中的常用字符。对于这之外的即使是合法的字符,也不支持显示。

3.不同程序对于字符编码的支持

不同程序对于字符编码的支持各异,下面分别以java和ruby为例进行简单说明:

Java编码

在Java内部支持unicode编码,严格地说它采用的是UTF-16编码,对于BMP字符严格支持,但对于增补字符U+100000-U+10FFFF支持不佳,在调用函数时需小心。具体参见3 4

在java中常见的中文string,length方法和substring方法都能得到正确的结果,但是对于增补字符会有问题,可能出现半个字的问题。Java中的length对于U+0000-U+FFFF的字符会认为是长度为1,但对于增补字符会认为是2。

String str = "今天天气很好" ;
Assert. assertEquals(6, str.length());
Assert. assertEquals("天气", str.substring(2, 4));
Assert. assertEquals(2, "\uDD1E\uD834".length());//增补特殊字符
Assert. assertEquals("\uD834", "\uDD1E\uD834".substring(1, 2));

Ruby编码

在ruby中1.9+采用原始文件的编码,可以在文件、io等进行指定,字符串长度等方法对于字符集可以很好的支持,对于增补字符也能得到正确的结果。具体参见5

"今天天气很好".length   =>  6
"\u{1DD1E}".length  => 1

4.其他的编码规范

URL编码:

在URI规范中存在一些保留字符,如:/?&=@%等,这些字符如果要作为参数使用而不是作为特殊字符使用,必须在%字符后以十六进制数值表示,例如%3A表示:;另外由于URL只能使用ASCII编码,所以对于中文等需要按照网页的编码格式进行编码,例如“中”应表示为%E4%B8%AD;另外URL 不能包含空格,通常使用 + 来替换空格。

XML编码:

在xml中对于特殊字符如 &<>’" 也需要进行转义。例如使用&lt;来表示<。在报文或是文件中需要对于这些特殊字符进行处理,如转义或是加上<![CDATE[ ]]>。否则在读取或保存是会出错。

注:严格地讲,在 XML 中仅有字符 “<”和”&” 是非法的。省略号、引号和大于号是合法的,但是把它们替换为实体引用是个好的习惯。 在XML的头部可以指定文件的编码方式,包括gbk,utf-8等,这些字符并不需要特殊转义,只要和声明的编码一致即可。

HTML编码:

和xml一样,对于&<>’”也需要进行转义处理。另外html对于其他一些不易输入的特殊字符可以使用特殊编码6 7。对于文件中的字符只要和文件头中声明的字符集一致即可。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》