「譯」懂點那啥編譯

原文compilers-for-free,主要用Ruby來描述,意譯為主,文章太長了,typo/翻譯錯誤等請留言,謝謝。
最後,對編譯器解釋器感興趣的可以看看

目錄

  • 介紹
  • 執行
  • 解釋
  • 編譯
  • 部分求值
  • 應用
  • 二村映射
  • 總結

介紹

我喜歡編程,尤其喜歡元編程。當Ruby開發者討論元編程時他們說的通常是“讓程序來寫程序”,因為Ruby有很多這方面的特性,如 BasicObject#instance_eval, Module#define_method還有BasicObject#method_missing,這讓我們寫的程序能在運行時新增功能。

但是我發現元編程還有另一種更有趣的方面:操縱程序表示的程序(programs that manipulate representations of programs)。這些程序將一些程序的源碼作為輸入,然後在其上做一些事情,如分析,求值,翻譯或者轉換。

這是一個由解釋器、編譯器和靜態分析器組成的世界,我覺得它很迷人,我們能編寫的最純粹、最自指(self-referential)的軟件。

我會給你展示這個世界,並讓你對程序有不同的看法。我不會說一些很複雜的技術,我只會說很酷的技術,告訴你操縱其他程序的程序是有趣的,並希望能激勵你繼續探索這方面的知識。

執行

我們從一個最熟悉的東西開始:程序執行。

假設我們有一個要運行的程序,以及一台運行它的機器。運行程序的第一步是把程序放到機器裏面。然後我們輸入一些數據作為程序的——命令行參數,或者配置文件,或者標準輸入,又或者其它的——總之我們把這些輸入放到了機器裏面。

原則上,程序在機器上執行,讀取輸入,併產生一些輸出:

但實際上,上圖只可能發生在那些硬件能直接執行該程序的情況。對於一個裸機,這意味着除非你的程序是用機器代碼寫的,否則就不可能像上圖那樣。(如果程序使用一些高級語言寫的,那麼機器要想理解這門語言,必須套一個語言虛擬機,比如Java字節碼之於JVM)。我們需要一個解釋器或者編譯器。

解釋

解釋器的大致工作機制如下:

  • 它讀取程序源碼,接着
  • 它解析程序源碼,構造出抽象語法樹(Abstract syntax tree,AST),最終
  • 解釋器遍歷AST並求值

我們試着構造一門玩具語言SIMPLE的解釋器,然後逐步討論。SIMPLE非常的直觀,類似於Ruby,但也不完全一樣。下面是一個SIMPLE的示例代碼:

a = 19 + 23

x = 2; y = x * 3

if (a < 10) { a = 0; b = 0 } else { b = 10 }

x = 1; while (x < 5) { x = x * 3 }

和Ruby不同的是,SIMPLE明確區分表達式語句。表達式求值得到一個值。比如表達式19 + 23求值得到42,表達式x * 3將當前x的值乘以3。語句求值不產生任何值,但是可能有副作用(side effect)導致當前的值被修改。對賦值語句a = 19 + 23求值會將a的值修改為42。

除了賦值外,SIMPLE還有一些語句:序列語句(挨個求值,逗號隔開),條件語句(if (…) { … } else { … }),循環語句(while (…) { … })。這些語句不會直接影響變量的值,但是它們的代碼塊中可能包含賦值語句,這些賦值語句就會影響變量的值。

有一個名為Treetop的Ruby庫,可以很容易的構造解析器,所以我們會使用Treetop來構造SIMPLE的解析器。(這裏不討論Treetop的細節,如果想了解更多請參見Treetop documentation)

在使用Treetop之前,我們還得寫一個語法文件,就像下面:

grammar Simple
  rule statement
    sequence
  end

  rule sequence
    first:sequenced_statement '; ' second:sequence
    /
    sequenced_statement
  end

  rule sequenced_statement
    while / assign / if
  end

  rule while
    'while (' condition:expression ') { ' body:statement ' }'
  end

  rule assign
    name:[a-z]+ ' = ' expression
  end

  rule if
    'if (' condition:expression ') { ' consequence:statement
      ' } else { ' alternative:statement ' }'
  end

  rule expression
    less_than
  end

  rule less_than
    left:add ' < ' right:less_than
    /
    add
  end

  rule add
    left:multiply ' + ' right:add
    /
    multiply
  end

  rule multiply
    left:term ' * ' right:multiply
    /
    term
  end

  rule term
    number / boolean / variable
  end

  rule number
    [0-9]+
  end

  rule boolean
    ('true' / 'false') ![a-z]
  end

  rule variable
    [a-z]+
  end
end

這個語法文件包含一些rules,它們很好的显示了表達式和語句有哪些。sequence rule是兩個用逗號隔開的語句,while rule表示一個由關鍵字while開頭,後面跟一個圓括號包裹的條件表達式,再後面跟一個花括號包裹的代碼體。assignment rule表示變量名跟一個等號,再跟一個表達式。然後number,boolean和variable這些rules分別表示数字字面值,布爾字面值和變量名字。

當我們寫完語法文件,就可以使用Treetop讀取它,Treetop會生成一個解析器。解析器的代碼用Ruby的class組織,所以我們可以實例化解析器,然後給它一個表示字符串讓它解析。
假設語法文件名字是simple.treetop,然後我們用IRB演示一下:

>> Treetop.load('simple.treetop')
=> SimpleParser

>> SimpleParser.new.parse('x = 2; y = x * 3')
=> SyntaxNode+Sequence1+Sequence0 offset=0, "x = 2; y = x * 3" (first,second):
     SyntaxNode+Assign1+Assign0 offset=0, "x = 2" (name,expression):
       SyntaxNode offset=0, "x":
         SyntaxNode offset=0, "x"
       SyntaxNode offset=1, " = "
       SyntaxNode+Number0 offset=4, "2":
         SyntaxNode offset=4, "2"
     SyntaxNode offset=5, "; "
     SyntaxNode+Assign1+Assign0 offset=7, "y = x * 3" (name,expression):
       SyntaxNode offset=7, "y":
         SyntaxNode offset=7, "y"
       SyntaxNode offset=8, " = "
       SyntaxNode+Multiply1+Multiply0 offset=11, "x * 3" (left,right):
         SyntaxNode+Variable0 offset=11, "x":
           SyntaxNode offset=11, "x"
         SyntaxNode offset=12, " * "
         SyntaxNode+Number0 offset=15, "3":
           SyntaxNode offset=15, "3"

解析器產生了一個名為具體語法樹(concrete syntax tree)的數據結構,也叫解析樹(parse tree),它包含了一大堆細節,但是實際上我們不需要這麼多細節,我們更希望一個抽象語法樹(abstract syntax tree),它包含更少的細節,更具表現力。

為了產出抽象語法樹,我們需要聲明一些類,它們是最後生成的抽象語法樹的節點。這些通過Struct很容易完成:

Number    = Struct.new :value
Boolean   = Struct.new :value
Variable  = Struct.new :name

Add       = Struct.new :left, :right
Multiply  = Struct.new :left, :right
LessThan  = Struct.new :left, :right

Assign    = Struct.new :name, :expression
If        = Struct.new :condition, :consequence, :alternative
Sequence  = Struct.new :first, :second
While     = Struct.new :condition, :body

現在,我們定義了一堆用來表示表達式和語句的node類:簡單的表達式如Number,Boolean只有包含一個value,二元表達式如Add,Multiply有left和right兩個表達式。Assign語句的name表示變量名,還有一個expression表示賦值語句的右側,等等等等。

Treetop還允許這些node類攜帶語義動作,這個語義動作是一個Ruby方法。我們可以用這個特性將具體語法樹的節點轉換為抽象語法樹的節點,讓我們把這個表示語義動作的方面命名為#to_ast

下面展示了如何定義number,boolean和變量的語義動作方法:

 rule number
  [0-9]+ {
    def to_ast
      Number.new(text_value.to_i)
    end
  }
end

rule boolean
  ('true' / 'false') ![a-z] {
    def to_ast
      Boolean.new(text_value == 'true')
    end
  }
end

rule variable
  [a-z]+ {
    def to_ast
      Variable.new(text_value.to_sym)
    end
  }
end

當Treetop應用number這條rule時,它繼續調用後面緊跟着的to_ast方法,這個方法把整数字面值轉換為一個整数字符串(使用String#to_i),然後用Number包裹創造處integer常量。#to_ast的實現在boolean和variable的rule中與之類似,它們最終都構造出合適的AST節點。

下面是一些二元表達式的#to_ast實現:

rule less_than
  left:add ' < ' right:less_than {
    def to_ast
      LessThan.new(left.to_ast, right.to_ast)
    end
  }
  /
  add
end

rule add
  left:multiply ' + ' right:add {
    def to_ast
      Add.new(left.to_ast, right.to_ast)
    end
  }
  /
  multiply
end

rule multiply
  left:term ' * ' right:multiply {
    def to_ast
      Multiply.new(left.to_ast, right.to_ast)
    end
  }
  /
  term
end

在上面三個例子中,#to_ast遞歸地調用具體語法樹的左右子表達式的#to_ast,將它們轉換為AST,然後把它們組合起來,放到一個類中。

語句也類似

rule sequence
  first:sequenced_statement '; ' second:sequence {
    def to_ast
      Sequence.new(first.to_ast, second.to_ast)
    end
  }
  /
  sequenced_statement
end

rule while
  'while (' condition:expression ') { ' body:statement ' }' {
    def to_ast
      While.new(condition.to_ast, body.to_ast)
    end
  }
end

rule assign
  name:[a-z]+ ' = ' expression {
    def to_ast
      Assign.new(name.text_value.to_sym, expression.to_ast)
    end
  }
end

rule if
  'if (' condition:expression ') { ' consequence:statement
    ' } else { ' alternative:statement ' }' {
    def to_ast
      If.new(condition.to_ast, consequence.to_ast, alternative.to_ast)
    end
  }
end

一樣的,#to_ast遞歸調用子表達式或者子語句的#to_ast將它們轉換為AST,然後組合成一個正確的AST類。

當所有的#to_ast都定義完成后,我們對具體語法樹的根節點調用#to_ast,遞歸地將整個樹轉換為AST:

>> SimpleParser.new.parse('x = 2; y = x * 3').to_ast
=> #<struct Sequence
     first=#<struct Assign
       name=:x,
       expression=#<struct Number value=2>
     >,
     second=#<struct Assign
       name=:y,
       expression=#<struct Multiply
         left=#<struct Variable name=:x>,
         right=#<struct Number value=3>
       >
     >
   >

儘管Treetop的設計目的是產出具體語法樹,但是用我們定義的node類和語義動作生成的AST是更簡單的結構。代碼x = 2; y = x * 3的AST如下圖所示:

它告訴我們x = 2; y = x * 3 是一個序列語句,包含兩個賦值語句:第一個將Number 2賦值給x,第二個將Variable x和Number 3相乘,即Multiply,結果賦值給y。

現在我們獲得類AST,可以遍歷它並求值。我們為每個AST node類定義一個#evaluate方法,它將對當前節點以及節點的子樹求值。當定義好所有的#evaluate方法后,可以調用AST樹的根節點的#evaluate求值。

下面是Number,Boolean,Variable的#evaluate定義

class Number
  def evaluate(environment)
    value
  end
end

class Boolean
  def evaluate(environment)
    value
  end
end

class Variable
  def evaluate(environment)
    environment[name]
  end
end

environment(環境)參數是一個哈希表,它將每個變量名和值關聯起來。比如說,一個 { x: 7, y: 11 }的environment表示有兩個變量,x和y,它們的值分別是7和11.我們假設每個SIMPLE程序的初始environment是一個空的哈希表。後面我們會看到語句求值如何改變environment。

當我們求值Number或者Boolean時,我們不關心它們是否在environment中,因為我們總是返回一個值,這個值是AST node構建的時候的那個值。如果我們對Variable求值,就得看看environment是否存在這個變量的值。

所以如果我們創建一個Number AST節點,然後用空environment開始求值,我們得到原始的Ruby整數:

>> Number.new(3).evaluate({})
=> 3

Boolean與之類似:

>> Boolean.new(false).evaluate({})
=> false

如果我們對一個名為y的Variable求值,並且environment是之前那個,那麼我們得到11,然而如果environment中的y的值是另一個,那麼對名為y的Variable求值也會得到另一個:

>> Variable.new(:y).evaluate({ x: 7, y: 11 })
=> 11

>> Variable.new(:y).evaluate({ x: 7, y: true })
=> true

下面是二元表達式的#evaluate定義

class Add
  def evaluate(environment)
    left.evaluate(environment) + right.evaluate(environment)
  end
end

class Multiply
  def evaluate(environment)
    left.evaluate(environment) * right.evaluate(environment)
  end
end

class LessThan
  def evaluate(environment)
    left.evaluate(environment) < right.evaluate(environment)
  end
end

這些實現遞歸地求值左右子表達式,然後對結果執行一些運算。如果是Add節點,那麼結果相加;如果是Multiply節點,結果相乘;如果是LessThan節點,結果用<運算符進行比較。

當我們對一個Multiply表達式x * y求值時,environment的x為2,y為3,那麼結果是6:

>> Multiply.new(Variable.new(:x), Variable.new(:y)).
     evaluate({ x: 2, y: 3 })
=> 6

如果在相同environment中對LessThan表達式x < y求值,得到結果true:

>> LessThan.new(Variable.new(:x), Variable.new(:y)).
     evaluate({ x: 2, y: 3 })
=> true

對於語句,#evaluate稍有不同。因為對語句求值會更新environment的值,而不是像表達式求值那樣簡單的返回一個值。表達式的#evaluate接受一個environment參數,返回一個新的environment,返回的environment與environment參數的差異就是語句的效果導致的。

直接影響變量的語句有Assign:

class Assign
  def evaluate(environment)
    environment.merge({ name => expression.evaluate(environment) })
  end
end

要求值Assign,我們得先求值右邊的表達式,得到一個值,然後使用Hash#merge創建一個修改后的environment。

如果我們在空environment中對x = 2求值,我們會得到一個新的environment,裡面包含了變量名x到值2的映射關係:

>> Assign.new(:x, Number.new(2)).evaluate({})
=> {:x=>2}

類似的,對y = x * 3求值,會在新的environment中關聯變量名y和值6:

>> Assign.new(:y, Multiply.new(Variable.new(:x), Number.new(3))).
     evaluate({ x: 2 })
=> {:x=>2, :y=>6}

對序列語句的求值是先求第一個語句,作為中間environment,然後求第二個語句,用中間environment作為第二個語句求值的environment,得到最終的environment:

class Sequence
  def evaluate(environment)
    second.evaluate(first.evaluate(environment))
  end
end

如果序列語句中有兩個賦值,那麼第二個賦值將是最終贏家:

>> Sequence.new(
     Assign.new(:x, Number.new(1)),
     Assign.new(:x, Number.new(2))
   ).evaluate({})
=> {:x=>2}

最後,我們還需要為If和While增加#evaluate

class If
  def evaluate(environment)
    if condition.evaluate(environment)
      consequence.evaluate(environment)
    else
      alternative.evaluate(environment)
    end
  end
end

class While
  def evaluate(environment)
    if condition.evaluate(environment)
      evaluate(body.evaluate(environment))
    else
      environment
    end
  end
end

If語句根據它的條件選擇求值兩個子語句中的一個。While在條件為true的情況下反覆對循環體求值。

現在,我們已經為所有表達式和語句實現了#evaluate,我們可以解析簡單的SIMPLE程序,然後得到AST。讓我們看看在空environment中對x = 2; y = x * 3求值會得到什麼:

>> SimpleParser.new.parse('x = 2; y = x * 3').
     to_ast.evaluate({})
=> {:x=>2, :y=>6}

正如期待的那樣,x和y的最終結果分別是2和6。

讓我們嘗試更複雜的程序:

>> SimpleParser.new.parse('x = 1; while (x < 5) { x = x * 3 }').
     to_ast.evaluate({})
=> {:x=>9}

這個程序將初始化x為1,然後反覆的將它翻三倍,直到它大於5。當求值結束,x的值為9,因為9是最小的大於等於5且三倍之於x的值。

以上,我們實現了一個解釋器:解析器讀取源代碼,生成具體語法樹,然後語法制導翻譯將具體語法樹轉換為AST,最後遍歷AST求值。

解釋器是單階段執行(single-stage execution)。我們可以將源程序和其它數據(在SIMPLE的例子中是初始的空environment)作為解釋器的輸入,然後解釋器基於輸入得到輸出:

所有的過程都發生在運行時——程序執行階段

編譯

現在我們了解了解釋器是如何工作的,那編譯器呢?

編譯器將源碼解析為AST,然後遍歷它,和解釋器一樣的套路。但是不同的是,編譯器不根據AST節點語義執行代碼,它是生成代碼。

讓我們寫一個SIMPLE到JavaScript的編譯器來解釋這個過程。現在不為AST node定義#evaluate方法,我們定義一個#to_javascript方法,這個方法將所有表達式和語句節點轉換為JavaScript代碼字符串。

下面是Number,Boolean,Variable的#to_javascript定義:

require 'json'

class Number
  def to_javascript
    "function (e) { return #{JSON.dump(value)}; }"
  end
end

class Boolean
  def to_javascript
    "function (e) { return #{JSON.dump(value)}; }"
  end
end

class Variable
  def to_javascript
    "function (e) { return e[#{JSON.dump(name)}]; }"
  end
end

上面的每個#to_javascript都會生成一個JavaScript的函數,這個函數接受一個environment參數e,然後返回一個值。對於Number表達式,返回的是Number所表達的整數:

>> Number.new(3).to_javascript
=> "function (e) { return 3; }"

我們可以獲取編譯后的JavaScript代碼,然後在瀏覽器console界面,或者Node.js REPL中調用JavaScript函數:

// 譯註:下面是JavaScript代碼
> program = function (e) { return 3; }
[Function]

> program({ x: 7, y: 11 })
3

類似的,Boolean AST節點編譯后也是一個JS函數,它返回true或者false:

>> Boolean.new(false).to_javascript
=> "function (e) { return false; }"

SIMPLE的Variable AST節點編譯後會在環境e中查找合適的變量,並返回這個值:

>> Variable.new(:y).to_javascript
=> "function (e) { return e[\"y\"]; }"

很顯然,上面的函數返回的內容取決於環境e的內容:

> program = function (e) { return e["y"]; }
[Function]

> program({ x: 7, y: 11 })
11

> program({ x: 7, y: true })
true

下面是二元表達式的#to_javascript實現

class Add
  def to_javascript
    "function (e) { return #{left.to_javascript}(e) + #{right.to_javascript}(e); }"
  end
end

class Multiply
  def to_javascript
    "function (e) { return #{left.to_javascript}(e) * #{right.to_javascript}(e); }"
  end
end

class LessThan
  def to_javascript
    "function (e) { return #{left.to_javascript}(e) < #{right.to_javascript}(e); }"
  end
end

我們將二元表達式的左右子表達式編譯成JavaScript的函數,然後外面再用JavaScript代碼包裹。當這段代碼真正執行的時候,左右子表達式的js函數都會被調用,得到值,然後兩個值做加法、乘法或者比較運算。讓我們以x * y為例,看看編譯過程:

>> Multiply.new(Variable.new(:x), Variable.new(:y)).to_javascript
=> "function (e) { return function (e) { return e[\"x\"]; }(e) *
    function (e) { return e[\"y\"]; }(e); }"

然後將編譯得到的JavaScript代碼(和上面一樣,只是看起來更美觀)放到支持的環境中運行:

> program = function (e) {
    return function (e) {
      return e["x"];
    }(e) * function (e) {
      return e["y"];
    }(e);
  }
[Function]

> program({ x: 2, y: 3 })
6

接下來是語句的#to_javascript,If用的是JavaScript的條件語句,While是JavaScript的循環,諸如此類。最外面和表達式一樣用一個JavaScript函數包裹,該函數接受一個環境e,語句可以更新環境。

class Assign
  def to_javascript
    "function (e) { e[#{JSON.dump(name)}] = #{expression.to_javascript}(e); return e; }"
  end
end

class If
  def to_javascript
    "function (e) { if (#{condition.to_javascript}(e))" +
      " { return #{consequence.to_javascript}(e); }" +
      " else { return #{alternative.to_javascript}(e); }" +
      ' }'
  end
end

class Sequence
  def to_javascript
    "function (e) { return #{second.to_javascript}(#{first.to_javascript}(e)); }"
  end
end

class While
  def to_javascript
    'function (e) {' +
      " while (#{condition.to_javascript}(e)) { e = #{body.to_javascript}(e); }" +
      ' return e;' +
      ' }'
  end
end

之前我們試過解釋器解釋x = 1; while (x < 5) { x = x * 3 },現在我們試試編譯器編譯:

>> SimpleParser.new.parse('x = 1; while (x < 5) { x = x * 3 }').
     to_ast.to_javascript
=> "function (e) { return function (e) { while (function (e)
   { return function (e) { return e[\"x\"]; }(e) < function (e)
   { return 5; }(e); }(e)) { e = function (e) { e[\"x\"] =
   function (e) { return function (e) { return e[\"x\"]; }(e) *
   function (e) { return 3; }(e); }(e); return e; }(e); } return
   e; }(function (e) { e[\"x\"] = function (e) { return 1; }(e);
   return e; }(e)); }"

儘管編譯后的代碼比源程序還長,但是至少我們已經將SIMPLE代碼轉換為了JavaScript代碼。如果我們在JavaScript的環境中運行編譯后的代碼,我們會獲得和解釋執行一樣的結果——x最終是9:

> program = function (e) {
    return function (e) {
      while (function (e) {
        return function (e) {
          return e["x"];
        }(e) < function (e) {
          return 5;
        }(e);
      }(e)) {
        e = function (e) {
          e["x"] = function (e) {
            return function (e) {
              return e["x"];
            }(e) * function (e) {
              return 3;
            }(e);
          }(e);
          return e;
        }(e);
      }
      return e;
    }(function (e) {
      e["x"] = function (e) { return 1; }(e);
      return e;
    }(e));
  }
[Function]

> program({})
{ x: 9 }

這個編譯器相當的傻——想讓它聰明些需要付出更多的努力——但是它向我們展示了如何在只能運行JavaScript的機器上執行SIMPLE程序。

編譯器是雙階段執行(two-stage execution)。首先我們提供源程序,編譯器接受它並生成目標程序作為輸出。接下來,我們拿出目標程序,給它一些輸入,然後允許它,得到最終結果:

編譯和解釋的最終結果是一致的——源代碼加輸入都最終變成了輸出——但是它將執行過程分成了兩個階段。第一個階段是編譯時,當目標程序生成后,第二個階段是運行時,這一階段讀取輸入和目標程序,最終得到輸出。

好消息是一個好的編譯器生成的目標程序會比解釋器執行的更快。將計算過程分成兩個階段免除了運行時的解釋開銷:解析源代碼,構造AST,遍歷AST樹這些過程都在編譯時完成,它們不影響程序運行。

(編譯還有其它性能受益,比如編譯器可以使用更簡潔的數據結構和優化讓目標程序跑的更快,尤其是在確定了目標程序運行的機器的情況下。)

壞消息是編譯會比解釋更難實現,原因有很多:

  • 編譯器必須要考慮兩個階段。當我們寫解釋器的時候,我們只需要關心解釋器的執行,但是編譯器還需要考慮到它生成的代碼在運行時的行為
  • 編譯器開發者需要懂兩門編程語言。解釋器用一門編程語言編寫,運行也是這一門語言,而我們的SIMPLE編譯器使用Ruby寫的,生成的目標程序代碼卻是JavaScript片段。
  • 將動態語言編譯為高效的目標程序生來就難。像Ruby和JavaScript這種語言允許程序在運行時改變程序自身或者新增代碼,因此程序源碼的靜態表示沒有必要告訴編譯器它想知道的關於自己在運行時的情況。

總結來說,寫解釋器比寫編譯器要簡單,但是解釋程序比執行編譯后的程序要慢。解釋器只有一個階段,只用一門語言,並且動態語言也是沒問題的——如果程序在運行時自身改變了,這沒問題,因為AST可以同步更新,解釋器也工作正常——但是這種簡潔和靈活性會招致運行時性能懲罰。

理想情況下我們想只寫一個解釋器,但是我們程序要想運行的快一些,通常還是需要再寫個編譯器。

部分求值

程序員總是使用解釋器和編譯器,但還有另一種操縱程序的程序不太為人所知:部分求值器。部分求值器是解釋器和編譯器的混合:

解釋器立即執行程序,編譯器生成程序稍後執行,而部分求值器立刻執行部分代碼,然後剩下的代碼稍後執行。

部分求值器讀取程序(又叫subject program)和程序的輸入,然後只執行部分代碼,這些代碼直接依賴輸入。當這些部分被求值后,剩下的程序部分(又叫residual program)作為輸出產出。

部分求值(partial evaluation)允許我們提取程序的一部分執行。現在我們不必一次提供程序的所有輸入,我們可以只提供一些,剩下的以後提供。

現在,我們不用一次執行整個subject program,我們可以將它和一些程序輸入放到部分求值器裏面。部分求值器將執行subject program的一部分,產生一個residual program;接下來,我們執行residual program,並給它剩下的程序輸入:

部分求值的總體效果是,通過將一些工作從未來(當我們打算運行程序時)移到現在,將單階段執行變成了兩個階段,程序運行時可以少執行一些代碼。

部分求值器將subject program轉換為AST,這一步和解釋器編譯器一樣,接着讀取程序的部分輸入。它分析整個AST找到哪些地方用到了程序的部分輸入,然後這些地方被求值,使用包含求值結果的代碼代替。當部分求值完成時,AST重新轉換為文本形式,即輸出為residual program。

真的構建一個部分求值器太龐大了,不太現實,但是我們可以通過一個示例來了解部分求值的過程。

假設有一個Ruby程序#power,它計算x的n次冪:

def power(n, x)
  if n.zero?
    1
  else
    x * power(n - 1, x)
  end
end

現在讓我們扮演一個Ruby部分求值器的角色,假設我們已經獲得了足夠的程序輸入,知道將用實參5作為第一個形參n調用power。我們可以很容易地生成一個新版本的方法,稱之為#power_5,其中形參n已被刪除,所有出現n的地方都被替換為5(這也叫常量傳播):

def power_5(x)
  if 5.zero?
    1
  else
    x * power(5 - 1, x)
  end
end

我們找到了兩個表達式——5.zero?5 - 1——這是潛在的部分求值對象。讓我們選一個,對5.zero?求值,用結果false代替這個表達式:

def power_5(x)
  if false
    1
  else
    x * power(5 - 1, x)
  end
end

這個部分求值行為使得我們還能再求值整個條件語句——if false ... else ... end總是走else分支(稀疏常量傳播):

def power_5(x)
  x * power(5 - 1, x)
end

現在對5 - 1求值,用常量4代替(常量摺疊):

def power_5(x)
  x * power(4, x)
end

現在停一下,因為我們知道#power的行為,這裡有一個優化機會,用#power方法的代碼代替這裏的power(4,x)調用(內聯展開),然後再用4代替所有形參n

def power_5(x)
  x *
  if 4.zero?
    1
  else
    x * power(4 - 1, x)
  end
end

情況和之前一樣,我們知道4.zero?,用false代替:

def power_5(x)
  x *
  if false
    1
  else
    x * power(4 - 1, x)
  end
end

繼續,知道條件語句走else分支:

def power_5(x)
  x *
  x * power(4 - 1, x)
end

知道4 - 1的值是3:

def power_5(x)
  x *
  x * power(3, x)
end

如此繼續。通過將#power調用內聯,然後對常量表達式求值,我們能用乘法代替它們,直到0:

def power_5(x)
  x *
  x *
  x *
  x *
  x * power(0, x)
end

讓我們再手動展開一次:

def power_5(x)
  x *
  x *
  x *
  x *
  x *
  if 0.zero?
    1
  else
    x * power(0 - 1, x)
  end
end

我們直到0.zero?是true:

def power_5(x)
  x *
  x *
  x *
  x *
  x *
  if true
    1
  else
    x * power(0 - 1, x)
  end
end

這是我們第一次在例子中遇到if true而不是if false的情況,現在選擇then分支,而不是else分支:

def power_5(x)
  x *
  x *
  x *
  x *
  x *
  1
end

最終效果是,我們把#power_5轉換為x重複乘以自身五次,然後乘以1。乘以1對計算沒有影響,所以可以消除它,最終得到的代碼如下:

def power_5(x)
  x * x * x * x * x
end

下面是圖形化的過程:

#power加上程序輸入5,最終經過部分求值,得到residual program#power_5。沒有新的代碼產生,#power_5的代碼是由#power組成的,只是重新安排並以新的方式結合在一起。

#power_5的優勢是當輸入為5的時候它比#power跑的更快。#power_5簡單的重複乘以x五次,而#power是執行了五次遞歸的方法調用,零值檢查,條件計算和減法。這些工作都由部分求值器完成,然後結果保留在residual program中,現在redisual program只依賴未知的程序輸入x。

所以,如果我們直到n為5,那麼#power_5就是#power優化改進版。

注意部分求值和部分應用(partial application)是不一樣的。基於部分應用的#power_5和部分求值得到的residual program是不一樣的:

def power_5(x)
  power(5, x)
end

通過部分應用,我們固定一個常量到代碼中,成功的將#power轉換為了#power_5,這個實現也是沒問題的:

>> power_5(2)
=> 32

但是這個版本的#power_5不見得比#power更高效——實際上它還會慢一些,因為它引入了額外的方法調用。

或者,我們也可以不定義新的方法,而是用Method#to_proc和Proc#curry將#power柯里化(currying)為一個過程,然後用實參5調用柯里化后的過程:

>> power_5 = method(:power).to_proc.curry.call(5)
=> #<Proc (lambda)>

接着,我們調用部分應用后的代碼,傳入的參數是#power的第二個形參,即x:

>> power_5.call(2)
=> 32

不難看出,部分應用和部分求值很像是,但是仔細審視后可以看出,部分應用是程序內的行為:當我們寫代碼時,部分應用允許我們固定一些參數,然後得到一個方法的更少參數的版本。而部分求值是程序外部的行為:它是一個源碼級別的轉換,通過特化一些程序輸入,可以得到更高效的方法的版本。

應用

部分求值有很多有用的應用場景。固定程序的一些輸入,即特化,可以提供兩方面的好處:我們可以編寫一個通用的、靈活的程序來支持許多不同的輸入,然後,當我們真正知道如何使用這個程序時,我們可以使用一個部分求值器來自動生成一個特化后的版本,該版本移除了代碼的通用性,但是提高了性能。

例如,像nginx或[Apache(http://httpd.apache.org/)這樣的web服務器在啟動時會讀取配置文件。該文件的內容會影響服務器後續每個HTTP請求的處理,因此web服務器必須花費一些執行時間來檢查其配置數據,以決定要做什麼。如果我們對web服務器使用部分求值器專門針對一個特定的配置文件進行部分求值,我們將得到一個新版本,它只執行該文件所說的操作;在啟動期間解析配置文件並在每個請求期間檢查其數據的開銷將不再是程序的一部分。

另一個經典的例子是光線跟蹤。要製作一部攝影機在三維場景中移動的電影,我們可能最終會使用光線跟蹤器渲染數千個單獨的幀。但是如果場景對於每一幀都是相同的,我們可以使用一個部分求值器來專門處理光線跟蹤器對於我們特定場景的描述,我們會得到一個新的光線跟蹤器,它只能渲染該場景。然後,每次我們使用專門的光線跟蹤器渲染幀時,都會避免讀取場景文件、設置表示其幾何體所需的數據結構以及對光線在場景中的行為做出與相機無關的決策的開銷。

一個更實際(有一些爭議)的例子是OpenGL管道。在OS X中,OpenGL管道的一部分是用LLVM中間語言編寫的,這其中包括一些GPU硬件就實現了的。不管GPU支持與否,都可以用軟件的方式實現,而有了部分求值,Apple公司可以對LLVM部分求值,刪除掉特定GPU上不需要的代碼,剩下的代碼是硬件沒有直接實現的。

二村映射

首先,我想說一個cool story。
1971年,Yoshihiko Futamura在Hitachi Central Research Laboratory工作時發表了一些有趣的論文。他在思考部分求值如何更早的處理subject program的一些輸入,然後產出後續可以執行的residual program。

具體來說,Futamura將解釋器作為部分求值的輸入並思考了一些問題。他意識到解釋器只是一個計算機程序而已,它讀取輸入,執行,然後得到輸出。解釋器的輸入之一是源碼,但是除了這個比較特別外解釋器和普通程序沒啥兩樣。

他在思考,如果我們先用部分求值器做一些解釋器的工作,會發生什麼?

如果我們使用部分求值器特化解釋器和它的某個程序的輸入,我們會得到residual program。然後,我們可以用程序的剩下的輸入加上residual program,得到最終結果:

最終效果和用解釋器直接運行程序一樣,只是現在單階段執行變成了兩階段。

Futamura注意到residual program讀取剩下的程序輸入,然後產生輸出,我們通常把這個residual program稱為目標程序——一個能被底層機器直接執行的源程序的新版本。這意味着部分求值器讀取源程序,產出目標程序,它實際上扮演了編譯器的角色。

看起來難以置信。它是怎麼工作的?讓我們看一個示例。下面是SIMPLE解釋器的top level環境:

source, environment = read_source, read_environment

Treetop.load('simple.treetop')
ast = SimpleParser.new.parse(source).to_ast
puts ast.evaluate(environment)

解釋器讀取源程序和初始化environment(從哪讀的我們不關心)。它加載Treetop語法文件,實例化解析器,然後解析器產出AST,然後對AST求值並輸出。#evaluate的定義和前面解釋器小結是一樣的。

讓我們想象一下將這個解釋器和SIMPLE源代碼 x = 2; y = x * 3作為輸入放入Ruby部分求值器。這意味這上面代碼中的#read_source將返回字符串 x = 2; y = x * 3,所以我們可以用字符串代替它:

source, environment = 'x = 2; y = x * 3', read_environment

Treetop.load('simple.treetop')
ast = SimpleParser.new.parse(source).to_ast
puts ast.evaluate(environment)

現在,讓我手動做一下複寫傳播,由於source變量只在代碼中用到了一次,我們可以完全消除它,用值代替:

environment = read_environment

Treetop.load('simple.treetop')
ast = SimpleParser.new.parse('x = 2; y = x * 3').to_ast
puts ast.evaluate(environment)

構造AST的數據都已經備起了,Treetop語法文件也準備好了,現在我們還知道待解析的字符串是什麼。讓我們手動求值表達式,創建一個手工AST代替解析過程:

environment = read_environment

ast = Sequence.new(
        Assign.new(
          :x,
          Number.new(2)
        ),
        Assign.new(
          :y,
          Multiply.new(
            Variable.new(:x),
            Number.new(3)
          )
        )
      )
puts ast.evaluate(environment)

我們將解釋器簡化為讀取環境、構建AST字面值、在AST根節點上調用#evaluate

在特定節點上調用#evaluate會發生什麼?我們早已直到每種節點的#evaluate定義,現在可以遍歷樹,然後找到AST節點的#evaluate調用,進行部分求值,和我們之前對#power做的差不多。AST包含了所有我們需要的數據,所以我們可以逐步將所有的#evaluate歸納為幾行Ruby代碼:

對於Number和Variable,我們直到它的值和變量名字,所以我們可以將這些信息傳播到其它節點。對於Multiply和Assign節點,我們可以內聯方法調用。對於序列語句,我們可以先內聯first.evaluate再內聯second.evaluate,使用臨時變量保持中間environment。

下面的代碼隱藏了大量的細節,但重要的是,我們擁有最終代碼和數據,它們可以幫助我們對AST根節點的#evaluate方法進行部分求值:

def evaluate(environment)
  environment = environment.merge({ :x => 2 })
  environment.merge({ :y => environment[:x] * 3 })
end

讓我們回到解釋器的主要代碼:

environment = read_environment

ast = Sequence.new(
        Assign.new(
          :x,
          Number.new(2)
        ),
        Assign.new(
          :y,
          Multiply.new(
            Variable.new(:x),
            Number.new(3)
          )
        )
      )
puts ast.evaluate(environment)

既然我們已經生成了AST根節點的#evaluate方法,現在我們可以對ast.evaluate(environment)部分求值,方法是展開這個調用:

environment = read_environment

environment = environment.merge({ :x => 2 })
puts environment.merge({ :y => environment[:x] * 3 })

這個Ruby代碼和原始的SIMPLE代碼產生了相同的行為:它將x設置為2,將y設置為x乘以3,x和y都存放到environment中。所以,從某種意義上說,我們通過對解釋器進行部分求值,將SIMPLE程序編譯為Ruby代碼——雖然還有一些其它environment的內容,但是總之,我們最終的Ruby代碼做的還是x = 2; y = x * 3這件事。

#power那個例子一樣,我們沒有寫Ruby代碼,residual program是重新安排解釋器的代碼並以新的方式結合在一起,這樣它就完成了與最初的SIMPLE程序相同的功能。

這種方式被稱為第一二村映射(first futamura projection):如果我們將源代碼和解釋器一起進行部分求值,我們將得到目標程序。

Futamura很高興他意識到了這一點。然後他繼續思考更多,他繼續意識到將源代碼和解釋器一起進行部分求值得到目標程序這件事本身,其實就是給一個程序多個參數。如果我們先使用部分求值來完成部分求值器的一些工作,然後通過運行剩餘程序來完成其餘的工作,會發生什麼情況?

如果我們用部分求值來特化部分求值器的一個輸入—— 解釋器——我們會得到residual program。然後後面我們可以運行這個residual program,將源程序作為輸入,得到目標程序,最後運行目標程序,傳入剩餘程序輸入,得到最終結果:

總體的效果與給出程序輸入,直接用解釋器運行源程序行為一致,但現在執行已分為三個階段。

Futamura注意到residual program讀取源程序產生目標程序,這個過程是我們常說的編譯器做的事情。這意味着部分求值器將解釋器作為輸入,輸出來編譯器。換句話說,部分求值器此時成了編譯器生成器。

這種方式被稱為第二二村映射(second futamura projection):如果我們將部分求值器和解釋器一起進行部分求值,我們將得到編譯器。

Futamura很高興他意識到了這一點。然後他繼續思考更多,他繼續意識到將部分求值器和解釋器一起進行部分求值得到編譯器這件事本身,其實就是給一個程序多個參數。如果我們先使用部分求值來完成部分求值器的一些工作,然後通過運行剩餘程序來完成其餘的工作,會發生什麼情況?

如果我們用部分求值來特化部分求值器的一個輸入—— 部分求值器——我們會得到residual program。然後後面我們可以運行這個residual program,將解釋器作為輸入,得到編譯器,最後運行編譯器,將源程序作為輸入得到目標程序,在運行目標程序,給它剩下的程序輸入,得到最終結果:

總體的效果與給出程序輸入,直接用解釋器運行源程序行為一致,但現在執行已分為四個階段。

Futamura注意到residual program讀取解釋器產生編譯器,這個過程是我們常說的編譯器做的事情。這意味着部分求值器產出來一個編譯器生成器。換句話說,部分求值器此時成了編譯器生成器的生成器。

這種方式被稱為第三二村映射(third futamura projection):如果我們將部分求值器和部分求值器一起進行部分求值,我們將得到編譯器生成器。

謝天謝地,我們不能再進一步了,因為如果我們重複這個過程,我們仍然會對部分求值器本身進行部分求值。只有三個二村映射。

結論

二村映射非常有趣,但不是說有了它編譯器工程師就是多餘的了。部分求值是一種適用於任何計算機程序的完全通用的技術;當應用於解釋器時,它可以消除解析源代碼和操作AST的開銷,但它不會自動地發明和創造具備工業級水平的編譯器的數據結構和優化。 我們仍然需要聰明的人寫編譯器,使我們的程序盡可能快地運行。

如果你想了解更多關於部分求值的知識,有一本叫做“Partial Evaluation and Automatic Program Generation”的免費書籍詳細的討論了它。LLVM的JIT,PyPy工具鏈的一部分——即RPython和VM以及它底層依賴的JIT)——也使用相關技術讓程序更高效執行。這些項目與Ruby直接相關,因為Rubinius依賴LLVM,還有一個用Python編寫的Ruby實現,名為Topaz,也依賴PyPy工具鏈。

撇開部分求值不談,Rubinius和JRuby也是高質量的編譯器,代碼很有趣,可以免費下載。如果你對操縱程序的程序感興趣,你可以深入Rubinius或JRuby,看看它們是如何工作的,並且(根據Matz的RubyConf 2013主題演講提到的)參与它們的開發。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

歐盟氣候研究組織:9月創歷來高溫紀錄

摘錄自2020年10月7日中央社報導

歐洲聯盟地球觀測計畫(Earth Observation Programme)今(7日)表示,今年9月地球表面的氣溫較歷來的9月溫暖,且1月以來的氣溫和史上最熱的2016年同一期間相差無幾。

根攄歐盟氣候監測機構「哥白尼氣候變化服務」(Copernicus Climate Change Service),今年有三個月創下高溫紀錄,分別是1月、5月和9月。

從去年9月至今年9月,地球溫度比工業化前高出將近攝氏1.3度。這項發現令人非常憂心,因為非常接近聯合國政府間氣候變遷問題小組(IPCC)2018年報告提出的攝氏1.5度門檻。報告中闡述地球升溫攝氏1.5度後將產生的重大衝擊。

氣候變遷
國際新聞
歐盟
記錄高溫

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

中國若要達2060年碳中和目標 得編150兆預算投入再生能源

摘錄自2020年10月8日自由時報報導

國際能源顧問公司伍德麥肯茲(Wood Mackenzie)今(8日)對中國2060年碳中和目標提出分析,表示此一目標中國恐將花超過新台幣150兆元經費,其中又會牽涉到煤礦省份未來所面臨的轉型問題,對此,該顧問公司認為,碳捕存(CCS)技術將成中國達成目標的關鍵,若少了碳捕存,則碳中和目標幾乎不可能。

據路透社報導,伍德麥肯茲公司今天表示,中國若要達到2060年碳中和目標,預計需要的投資金額將超過5兆美元(約為新台幣150兆元),其中包含發展再生能源發電設施等,中國國家主席習近平9月在聯合國成立75週年視訊大會上表示,中國要在2060年達到「碳中和」目標,即為碳的排放量會等於減少量。目前,中國是世界上最大的碳排放國,約佔全球排放量的28%。

氣候變遷
國際新聞
中國
碳中和
碳捕存技術

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

開着容易上癮,這3款10來萬的合資車,比朗逸好玩得多

6L的動力版本,會讓你在日常代步之餘,也能享受到小鋼炮的魅力。至於1。5T的,朋友,秋名山見。福克斯的短板在於儲物空間較為一般,後排在兩廂車這個級別裏面算是表現一般,中下的水平。致悅,沒錯,不是致炫,是來自菲亞特选手的致悅,至於為啥會推薦它,不是它表現的多突出,主要是價格便宜,優惠還大,這是許多人買車考慮的問題之一。

緊湊型車三廂的感覺操控不過來?

有便宜的兩廂車推薦嗎?

想買兩廂車怎麼辦?

有哪些兩廂車選擇?

首先是來自馬自達选手帶來的昂科賽拉,對於這款車大家都不陌生,操控就不說了,兩廂車操控還不好也不好意思推薦了,昂科賽拉的底盤懸挂是想要表揚的,一個字,穩,即使是高速過彎,你也不會覺得虛。

在眾多車企紛紛投入渦輪增壓發動機的大軍中去是,馬自達堅持做自吸發動機,這簡直是個異類,但是馬自達憑藉創馳藍天技術使得昂科賽拉在動力方面不遜色於搭載渦輪發動機的車型,而且還省油,這也是馬自達敢拿自吸發動機打着運動旗號的原因了。

後排較小,是昂科賽拉的短板,這也是許多馬自達車型的短板了,買馬自達的朋友們要注意哦~

另外是昂科賽拉在胎躁控制方面的還是不夠,這是許多車主反應的問題了。

福特选手帶來的則是福克斯,福克斯是大家心目中的理想車型之一了,美系車的隔音好,配置豐富的特點都具備,還有眾多的動力選擇。

1.0T的動力總成,對於日常上下班代步通勤的朋友們,會說,夠用!1.6L的動力版本,會讓你在日常代步之餘,也能享受到小鋼炮的魅力!至於1.5T的,朋友,秋名山見!

福克斯的短板在於儲物空間較為一般,後排在兩廂車這個級別裏面算是表現一般,中下的水平。

致悅,沒錯,不是致炫,是來自菲亞特选手的致悅,至於為啥會推薦它,不是它表現的多突出,主要是價格便宜,優惠還大,這是許多人買車考慮的問題之一。

除了價格之外,致悅的空間表現以及動力表現也是挺不錯的,全系搭載1.4T渦輪增壓發動機,動力能說不夠嗎?用車主的話說,就是一給油就跑!

從車身尺寸以及乘坐空間測試來看,致悅的表現在同級別不能說最好,但是中上水平還是可以有的。而讓人不滿意的是所搭配的DCT雙離合變速器在低擋換擋的時候頓挫感比較明顯。

以上三款車都是所推薦的兩廂車,綜合實力來說都是比較強的,各位看官可以根據自己的需求來挑選喲!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

【Spring註解驅動開發】面試官:如何將Service注入到Servlet中?朋友又栽了!!

寫在前面

最近,一位讀者出去面試前準備了很久,信心滿滿的去面試。沒想到面試官的一個問題把他難住了。面試官的問題是這樣的:如何使用Spring將Service注入到Servlet中呢?這位讀者平時也是很努力的,看什麼源碼啊、多線程啊、高併發啊、設計模式啊等等。沒想到卻在一個很簡單的問題上栽了跟頭,這就說明學習知識要系統化,要有條理,切忌東學一點,西記一點,否則,到頭來,啥也學不到。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

如何實現將Service注入到Servlet中??

這裏,我們列舉兩種解決方法(推薦使用第二種)

方法一:

直接重寫Servlet的Init()方法,代碼如下:

public void init(ServletConfig servletConfig) throws ServletException {
	ServletContext servletContext = servletConfig.getServletContext();
	WebApplicationContext webApplicationContext = WebApplicationContextUtils
			.getWebApplicationContext(servletContext);
	AutowireCapableBeanFactory autowireCapableBeanFactory = webApplicationContext
			.getAutowireCapableBeanFactory();
	autowireCapableBeanFactory.configureBean(this, BEAN_NAME);
}

這裏的BEAN_NAME即為我們需要注入到Spring容器中的服務,但這並不是一個好的方法,因為我們需要在每一個Servlet中都進行這樣的操作。

方法二:

我們可以寫一個類似於“org.springframework.web.struts.DelegatingRequestProcessor”的委託的Bean,然後通過配置的方法把我們的服務注入到servlet中,具體方法如下,

Step 1:編寫委託類DelegatingServletProxy

package com.telek.pba.base.util;

import java.io.IOException;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * 以下是類似org.springframework.web.struts.DelegatingRequestProcessor的一個委託
 * 用於通過配置的方法,在Servlet中注入Service
 * @author binghe
 * */
public class DelegatingServletProxy extends GenericServlet{
    private static final long serialVersionUID = 1L;
    private String targetBean;
    private Servlet proxy;

   @Override
   public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
   		proxy.service(req, res);
   }
     /**
      * 初始化
      */
      public void init() throws ServletException {
          this.targetBean = getServletName();
          getServletBean();
          proxy.init(getServletConfig());
      }

     /**
      * 獲取Bean
      */
      private void getServletBean() {
          WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
          this.proxy = (Servlet) wac.getBean(targetBean);
      }
}

Step 2:修改Web.xml配置

在純Servlet模式下,我們的配置方式如下(以下由於代碼高亮插件的問題,請將代碼中的#替換成尖括號)

<servlet>
  <description>活動發起模塊活動查詢分頁Servlet</description>
  <display-name>launchActivityQueryServlet</display>
  <servlet-name>LaunchActivityQueryServlet</servlet-name>
  <servlet-class>com.telek.pba.launch.servlet.LaunchActivityQueryServlet</servlet-class>
<servlet>

<servlet-mapping>
  <servlet-name>LaunchActivityQueryServlet</servlet-name>
  <url-pattern>/servlet/launch/LaunchActivityQueryServlet</url-pattern>
</servlet-mapping>
</servlet>

如果採用我們這種代理的方法,則配置應該修改為:

<servlet>
  <description>活動發起模塊活動查詢分頁Servlet</description>
  <display-name>launchActivityQueryServlet</display>
  <servlet-name>launchActivityQueryServlet</servlet-name>
  <servlet-class>com.telek.pba.base.util.DelegatingServletProxy</servlet-class>
<servlet>

<servlet-mapping>
  <servlet-name>launchActivityQuery</servlet-name>
  <url-pattern>/servlet/launch/LaunchActivityQueryServlet</url-pattern>
</servlet-mapping>
</servlet> 

注意:默認情況下,Servlet的配置中,LaunchActivityQuery的首字母一般為大寫,而我們的標題中已註明,我們採用Spring的註解模式,如果是自動掃描註解的話,默認情況下,註解的value值為首字母小寫,即:launchActivityQuery,因此,在我們新的配置中,要注意將首字母改為小寫,否則會報無法找到Bean的錯誤。

Step 3:至此,我們就可以像SSH的注入方式一樣,注入Servlet了,以下是個小示例:

package com.telek.pba.launch.servlet;

import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import com.telek.pba.base.model.PbaUserInfo;
import com.telek.pba.launch.dao.IPbaActivityInfoCurrentDAO;

@Component
public class LaunchActivityQueryServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	 
	//注入IPbaActivityInfoCurrentDAO
	@Resource
	private IPbaActivityInfoCurrentDAO pbaActivityInfoCurrentDAO;

	public LaunchActivityQueryServlet() {
		super();
	}
	 
	public void destroy() {
		super.destroy(); // Just puts "destroy" string in log
		// Put your code here
	}
	 
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//sth to do
	}
	 
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//sth to do
	}
	 
	public void init() throws ServletException {
		// Put your code here
	}
}

最後,請留心在Spring配置文件中,配置上自動掃描包的路徑:

<context:component-scan base-package="com.telek.pba.*.dao.impl,
 					com.telek.pba.*.service.impl,
 					com.telek.pba.*.servlet"/>

大功告成!

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

python工業互聯網應用實戰2—從需求開始

前言:隨着國家工業2025戰略的推進,工業互聯網發展將會提速,將迎來一個新的發展時期,越來越多的企業開始逐步的把產線自動化,去年年底投產的小米亦庄的智能工廠就是一個熱議的新聞。小米/華為智能工廠只能說是中國製造2025的一個代表,產業轉型和製造升級,筆者從事的企業領域就越到越來越多的(製造)企業開始悄悄的自動化/智能化。這裏肯定有國家政策推動的大背景,同時,也有着企業自身不斷提高生產率的“剛需”。

  本序列我們也從一個需求問題開始;然後,拆解需求(問題);其次,解決拆解需求(問題)的點;再次,通過的不斷技術摸索和迭代過程找到一個個合理的解決需求的方法和手段,最終,完成了這個需求(問題)的項目實戰。我們會在文中描述演化過程、方法論、過程保證機制和實用的工具等,最終帶着大家完成這樣一個實際項目需求的項目過程項目涉及的Django的多基礎知識請大家閱讀筆者早期的《Python開發入門與實戰系列》。

  本文的過程採用python3.6和django2.1版本

項目需求:這是一個簡版物流自動化倉庫實例,倉庫控制系統(以下簡稱WCS)需要調度AGV小車運送一個實托盤(搬運單元)從1樓入庫站台經由提升機搬運到2樓指定的倉庫貨位。 

1.需求分析

  倉庫控制系統(以下簡稱WCS)需要調度AGV小車運送一個實托盤(搬運單元)從1樓入庫站台經由提升機搬運到2樓指定的倉庫貨位。這句簡要描述常常是我們在項目URS看到的需求描述,接下來我們需要對拿到的需求進行分析,形成一個個可度量的開發功能點。

1.1.用例說明

  需求用例說明文檔能夠很好的對需求進行詳細的分析,主要的包含內容包括:前置條件、事件流程和後置條件(執行結果)用例描述如下錶,通過用例分析我們能夠很好的把握需求的具體事件執行流程,並通過文檔清晰的描述出來,便於後期設計編碼時作為開發設計人員的指導。

  經過團隊頭腦風暴討論,大家基本上達成了如下錶的需求用例說明,我們把這個調度設備的搬運過程設計成一個序列順序執行的步驟(子任務),這些子任務對應着設備的執行分解動作。

用例編碼

1.1

執行角色

WCS

用例名稱

托盤入庫用例

前提條件

  1. WMS已完成任務貨位的分配,任務核心信息:托盤條碼、源地址、目標地址;
  2. AGV不能跨樓層作業,1樓與2樓分別是不同的AGV小車執行任務;

需求描述

1 WCS分解搬運任務成3個設備作業

2 調度AGV入庫站台搬運托盤到提升機1樓門口工位;

3 調度提升機到1樓並打開廊門

4 調度AGV提升機1樓門口工位到提升機並卸貨,返回提升機門口工位;

5 調度提升機1樓提升到2樓並開門;

6 調度AGV進入提升機並載貨搬運到2樓門口工位;

7 調度提升機關門;

8 調度AGV從2樓門口工位搬運到庫存貨位。

備選過程

 

擴展過程

 

 

原始需求描述

參見 《XXX URS》

特殊需求

後置條件

WCS調度相關設備完成實托盤從入庫站台搬運到庫存貨位,反饋結果給WMS 

2. 需求功能點

  從上面的用例說明的需求描述事件流程來看,我們需要先把這一趟搬運任務分解成設備子任務,並串行的方式順序下達到設備執行,任務清單如下錶:

序號

作業描述

執行設備

1

調度AGV入庫站台搬運托盤到提升機1樓門口工位

1AGV

2

調度提升機到1樓並打開門

提升機

3

調度AGV提升機1樓門口工位到提升機並卸貨,返回提升機門口工位

1AGV

4

調度提升機1樓提升到2樓並開門

提升機

5

調度AGV進入提升機並載貨搬運到2樓門口工位

2AGV

6

調度提升機關門

提升機

7

調度AGV從2樓門口工位搬運到庫存貨位

2AGV

  也就是說這一趟搬運任務,WCS需要分解成7個設備作業子任務,並順序下達給相應的執行設備執行,最終完成任務的執行(當前的任務劃分粒度實際對接AGV和提升機廠家來說會有調整,最終以上步驟會依賴與實際對接的情況,但是主流程不會有太大變化)。 

   經過分析從1樓入庫站台運送托盤到二樓某個指定貨位這樣一個任務,系統需要分解成7個子任務,下達給設備順序執行。系統活動圖如下:

3. 活動圖

  經過分析從1樓入庫站台運送托盤到2樓某個指定貨位這樣一個任務,系統需要分解成7個子任務,下達給設備順序執行。我們還可以通過UML活動圖來進一步詳細的描述作業的執行順序如下圖: 

    從圖中我們可以看出來一次入庫任務,系統分解為7個設備子任務(作業)來執行完整的托盤入庫流程,只有所有子任務(作業)執行完成,托盤的入庫才算完成。

 4. 功能模塊

  對於這樣一個看似簡單的需求來說,包含兩大主要功能模塊

    1. 任務分解:依據物理設備處理任務的條件,對任務進行分解,任務分解的粒度是設備能夠識別並執行(動作)
    2. 任務調度:任務調度就是按照順序執行的邏輯,把任務順序和逐一下達給設備 

  這裏也有幾個基本邏輯就是,設備在某一個時間點上只能執行一個子任務,只有這個任務執行完畢後方能下達新的子任務。多重任務邏輯只會導致設備無法完成任務(不知道到底該執行那個動作)。

 4.1. 實體關係

  我們從上面需求分析整理當前至少包括2個實體,包含的屬性(字段)如下: 

1任務

任務ID,任務號,源地址(從哪兒),目標地址(到哪兒),開始時間,結束時間,狀態 

2子任務:

子任務ID,任務ID,源地址(從哪兒 上一個子任務的目標地址), 目標地址(到哪兒 下一個子任務的源地址), 執行機構,開始時間,結束時間,狀態

5. 小節

  本章我們從一個需求問題開始,然後經過需求分析,把需求問題分解為功能點和數據實體,實體是下一步我們設計表或ORM model的基礎原型,上面的實體只是一個初步的需求分析主要字段要求,實體屬性(字段)會設計時會增加特定的其它屬性(字段)。 下一章節我們將描繪如何把這個需求逐步演化到模型設計。

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

吉力馬札羅山山坡大火 坦尚尼亞消防員連日撲救

摘錄自2020年10月13日中央社報導

東非國家坦尚尼亞官員表示,消防員連續第3天與吉力馬札羅山(Mount Kilimanjaro)山坡上的大火對抗,希望控制這座非洲第一高峰的火勢。

路透社報導,大火11日起於胡娜區(Whona),強風助長了火勢,官員表示,風勢今天已經減弱。胡娜區是使用曼達拉(Mandara)和霍倫坡(Horombo)兩條登山路線的登山客的休息中心。

坦尚尼亞國家公園管理局(TANAPA)官員謝魯特(Pascal Shelutete)向路透社表示,消防員與相關人員正努力控制火勢,並稱火勢已「幾乎獲遏止」。

氣候變遷
國際新聞
坦尚尼亞
吉力馬扎羅山
大火

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

澳洲科學家發現巨型珊瑚礁 比帝國大廈還高

摘錄自2020年10月29日中央社報導

施密特海洋研究所(Schmidt Ocean Institute)本週表示,澳洲科學家在大堡礁發現一座獨立珊瑚礁,高度超越紐約帝國大廈和巴黎艾菲爾鐵塔,為120多年來首見。

這座「刀片狀」珊瑚礁將近500公尺高、1.5公里寬,頂端位於海平面下40公尺。由畢曼(Robin Beaman)博士率領的詹姆士庫克大學(James Cook University)科學家團隊20日在科研船福克爾號(Falkor)上,於大堡礁北方海床進行繪測作業時發現這座珊瑚礁。

科學家利用水下機器人SuBastian錄下他們探索這座新珊瑚礁的過程,一路上也在蒐集海洋樣本,之後將會存放在昆士蘭博物館(Queensland Museum)和昆士蘭熱帶博物館(Museum of Tropical Queensland)。

海洋
國際新聞
澳洲
珊瑚礁

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

哈弗H6最強對手換代,天窗無敵,內飾升級,9.98萬你買嗎?

值得一提的是,全景影像四攝像頭還具備行車記錄儀功能,防護功能遠勝傳統行車記錄儀。此外,法國法雷奧一鍵泊車系統(12顆超聲波雷達)、鑰匙遙控泊車等功能在同級更是前所未見,勇氣可嘉。配置方面,前排座椅加熱、通風和空調功能被集成至排擋桿前方的獨立觸控屏上,估計出於成本控制輕觸后未見震動反饋,增加了盲操學習成本,所幸保留了聲音反饋。

滿足小有成就的現狀,缺乏居安思危意識,是營銷領域中不少先鋒產品的通病。等恍悟備受衝擊、該有所行動時,卻被時代洪流淹沒的例子,比比皆是。

(老款CS75)

上市四年以來,長安CS75無論在群眾口碑抑或銷量方面,都頗有建樹。隨着時間推移、消費升級,外觀和內飾開始顯得落伍、俗套。為了喚起潛在買家熱情,中期改款CS75厲兵秣馬、煥新而來。

全新CS75能否追隨自主SUV往上發展、實現高端化的潮流?身體力行前往世外桃源香格里拉,一試究竟。

外觀設計

長安睿騁CC砍獲德國紅點設計大獎后名噪一時,飄逸流暢的蝶翼式前臉讓長安轎車產品觀感度躍升一個維度。這份美好卻未能沿襲至全新CS75上,引以為憾。

所幸的是,新款CS75內外設計均往积極方向在推進。外觀一改老款地盤包工頭的“油膩”印象,大量的橫向鍍鉻飾條強調年輕化,比頗具爭議的“CHANGAN”字母臉強不止一星半點。

車燈儼然成為時下車型外觀設計的重中之重,深諳於此的長安為新款CS75換裝帶自動大燈功能的全LED大燈,造型犀利、燈腔內部精緻,跟來車怒目相視的感覺。

箭羽造型的霧燈區域安放在前包圍左右兩側,LED轉向燈帶巧妙地鑲嵌其上,內凹線條的勾勒對運動感的提升大有裨益,成功化解“中年危機”。

新款車型配有ACC自適應巡航功能,車頭底部的模塊能窺探一二,較強的塑料感缺乏精緻度。

車身尺寸與舊款別無二致,長/寬/高:4650/1850/1705mm,軸距:2700mm。側腰線條平鋪直敘、寬厚瓷實,屬四平八穩的類型。

后視鏡集成了轉向燈,從底部的攝像頭和鏡面標識可知,新款高配車型配備了盲區監測功能和全景影像。

試駕車型選用19英寸雙色鋁合金輪圈,與之搭配的是優科豪馬GEOLANDAR G91輪胎(225/55 R19),為四驅輪胎開發的獨特胎面膠配方,溝槽設計主打濕地操控、制動性能,乾地制動表現一般。

自主SUV一窩蜂玩壞了懸浮式車頂、碩大英文尾標后,又將魔爪伸向了當年林肯MKZ敢為人先的貫穿式尾燈。不出多久,貫穿式尾燈會淪為自主SUV中俗套的設計元素。

選用貨真價實的雙出排氣布局,扁平排氣口造型不落窠臼,與反光帶、尾燈造型遙相呼應,呈現出整體感喜人的尾部輪廓。

內飾配置

與外觀一樣,全新CS75的內飾表現出足夠的革新度和顛覆感,舊款內飾的鄉村氣息黯晦消沉,取而代之的是與時俱進的內飾氛圍。

軟質材料應用廣泛,多處的單雙縫線交替,方寸之間,盡顯品位。新車內飾供科技黑和輕奢棕兩色可選,後者配備仿木紋材料更是時刻在強調檔次感。

10.25英寸全液晶儀錶盤、10.25英寸懸浮式觸控屏、电子擋桿、平底方向盤、大面積的鋼琴烤漆面板、七色氛圍燈對車內質感的提升大有裨益。

其中試駕車型的10.25英寸懸浮式觸控屏配備全景影像和虛擬影像功能,显示效果細膩,提供優良閱讀效果,可謂女司機福音。

以配置水平見長的自主車,無鑰匙進入/啟動功能自然必備必不可少,全系標配德爾福STT智能啟停功能,優化燃油經濟性。

遠不止此,強調科技感的全液晶儀錶盤能與中控大屏實現雙屏互動,導航、ACC全速自適應巡航(0-150km/h)等信息同樣能呈現於儀錶盤,甚是方便。

值得一提的是,全景影像四攝像頭還具備行車記錄儀功能,防護功能遠勝傳統行車記錄儀。此外,法國法雷奧一鍵泊車系統(12顆超聲波雷達)、鑰匙遙控泊車等功能在同級更是前所未見,勇氣可嘉。

配置方面,前排座椅加熱、通風和空調功能被集成至排擋桿前方的獨立觸控屏上,估計出於成本控制輕觸后未見震動反饋,增加了盲操學習成本,所幸保留了聲音反饋。

溫度調節和吹風模式保留實體按鍵,長安工程師對人機工程學的考究是可感知的。但如何改善光滑面板的反光現象,會是道不小的難題。

最為人稱道的當屬換裝电子擋桿,採用輕觸方式激活p擋的方式同級罕見,但使用體驗有待商榷,需要適應一段時間。

貴為長安產品矩陣中的銷量擔當,新款CS75僅前排車窗支持一降升降功能,為車內乘員日常啟閉車窗增添不便,不應當,扣分項。

空間表現

國人喜聞樂見的全景天窗如常出席,1.12平方米的採光面積(同級最大)有效拉升乘員的暈車閾值。輔以靠背支持多角度調節的後排座椅以及接近純平的後排中央地板,絕對是說服長輩掏包的有力論證。

整體空間表現絲毫不怵主流中型SUV水準,不易聽到乘員的抱怨聲。手套箱、扶手箱足以安放常規單反相機,擋桿前方、杯架、四門門把手處的儲物格均能容納大尺寸智能手機,實屬難得。

此外,後排座椅安全帶卡扣可收折,不磕屁股。後排座椅支持4/6分割放倒,放倒後接近純平狀態,宜商宜居。

動態表現

新款CS75搭載全新藍鯨 280T 直噴增壓發動機,這枚1.5T發動機在德爾福高壓共軌直噴系統、霍尼韋爾增強脈衝渦輪增壓器、水冷中冷的加持下,最大馬力178ps/5500rpm、峰值扭矩265N·m/1450-4500rpm,官宣百公里加速時間為10s,搭配日本愛信第三代6AT。

此前舊款CS75機油增多事件牽動不少潛在買家的心,而全新CS75搭載的發動機選用全新的噴油方案以及熱管理系統,暖機時間較此前縮短近2min,輔以重新調校的ECU程序,各位無需過慮。

起步階段存有“起床氣”,若不將油門踩得比預期稍深,車輛更願意停留在慵懶狀態。高速下再深踩油門,引擎艙發出歇斯底里的嘶吼聲,小排量渦輪增壓的單薄感被進一步放大。

變速箱齒比設定綿密,3擋開始便能實現閉鎖控制,對動力輸出的連續性、降低動力損耗等大有裨益。升擋后發動機轉速小幅回落,努力維持在峰值扭矩輸出區間,兢兢業業地保持加速力道。至於推背感嘛,沒有。

剎車前半段虛位較大,制動力輸出不夠線性,日常市區行駛不易形成輕鬆駕駛氛圍。更青睞一點就有、漸進式的剎車腳感,還望廠商後期優化。

新車選用前麥弗遜、后多連桿獨立懸架結構,配備美國天納克MTV-CL減震器閥系,彈簧K值較同級家用SUV高,在面對坑窪、起伏路面時動作稍顯敏感。重心轉移與駕駛員預期不夠默契,但勝在拋跳抑制處理到位。

一鍵泊車、鑰匙遙控泊車、遠程控制(發動機、空調、空氣凈化、遠程防盜追蹤)、后追尾預警、開門碰撞預警、隨動氛圍燈、同級最大天窗、电子擋把、安第斯灰車漆,這些同級罕見甚至前所未見的配置,都被新款CS75一一用上。

(售價:9.98-15.98萬元)

相信這是CS75深思熟慮后的改進,因為長安深知顏值拼不過WEY VV5,內飾氛圍、机械規格也難敵領克01,為此選擇在產品年輕化、科技互聯環節大下苦功,清晰的定位有望搶佔豪華自主SUV的年輕潛在買家。

相近的預算,CS75要說服國人不選合資緊湊型SUV,尚屬馳高鶩遠。我想,大概還差一份如逸動、睿騁CC般驚艷、精緻、創新的設計手稿吧。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

90后買起來也輕鬆,10萬出頭買這些車你才不會後悔

這一部分人則跟表現中庸之道的卡羅拉契合,沉穩並不是中年人的詞彙,成家立業的年輕人同樣需要這份性格。辣評:卡羅拉的外觀不像思域那麼激進,極高辨識度的豐田前臉讓你開出去並不會掉面子。之所以用沉穩來形容卡羅拉,除了外觀之外,簡潔的內飾、不急不緩的動力表現、穩定的油耗都是卡羅拉的特點。

90后已經成為消費的主力軍,為什麼汽車在外觀的設計上越來越趨向於年輕化、運動化,就是為了迎合年輕消費者的口味,因而汽車變得個性分明。人有性格,車也有性格,我們所說的磨合期就是源自於此。為什麼二手車市場出現這麼多准新車,不是因為汽車出了事故,或者發了大財想要換新車,而是車主發現車並不適合自己。那麼買車為什麼不買契合自己性格的呢,確認過性格,找到對的車。

經濟崛起的這批年輕人或熱血或沉穩或放浪不羈或剛中帶柔……靠着努力與堅持,一步步走向成功,豪車目前對他們來說還很遙遠,10-15萬這個級別恰到好處,經濟上不會那麼吃力。那麼接下來要介紹的車符不符合你的性格?一起來看一下。

熱血思域

十代思域銷量持續增長的原因之一是前幾代思域口碑的加持,但認為最大的原因在於它年輕的外表加熱血的心。外觀上對年輕人有着極大的吸引力,車身低趴,線條之間搭配得很好,車頭線條柔和,腰線則凌厲。跟現在有個性的年輕人一樣,衣着光鮮,打扮新潮,如果你認為它是安分守己的乖孩子,那你就大錯特錯了。

現在很多廠家的宣傳往往都會打上秒天秒地秒思域的名頭,但是他們從來不敢說輸了直接燒車這樣的大話,因為他們旗下所謂的神車全都抵擋不了思域那顆火熱的“心”。思域1.0T、1.5T的發動機同屬本田當家技術-地球夢系列發動機,只不過1.0T是3缸發動機,不要因為它是三缸就小覷它。須知道缸內直噴、進排氣可變正時系統、進氣側可變升程技術、變排量機油泵等主流發動機技術都出現在它的身上,在保證燃油經濟性的同時,也為它帶來了125ps的動力;1.0T尚且如此,更不用說更為強大的1.5T 177ps的發動機。

辣評:在內飾、配置同級別都差不多的車型里,思域憑藉著較為強大的動力,再加上與CVT、6擋手動變速箱之間的配合,車主在享受速度的同時,換擋也比較順滑,一個同樣有衝勁的人可以掌控這台有衝勁的車,這才是這麼多人選擇它的理由,不知道年輕的你會做出怎樣選擇。

沉穩卡羅拉

思域不是唯一的選擇,還有同樣優秀的卡羅拉,一開始就說過,選擇契合自己性格的車。當然不是每一個人都這麼熱血衝動,有的人性格沉穩,遇事不急不緩。這一部分人則跟表現中庸之道的卡羅拉契合,沉穩並不是中年人的詞彙,成家立業的年輕人同樣需要這份性格。

辣評:卡羅拉的外觀不像思域那麼激進,極高辨識度的豐田前臉讓你開出去並不會掉面子。之所以用沉穩來形容卡羅拉,除了外觀之外,簡潔的內飾、不急不緩的動力表現、穩定的油耗都是卡羅拉的特點。你很難在這輛車上找出明顯的短板,同樣,令人側目的亮點也沒有。總的來說,這樣一輛車適合那些低調又不失格調的成熟人士。

放浪不羈自由本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?